zio-jdbc icon indicating copy to clipboard operation
zio-jdbc copied to clipboard

Replace Schema-based derivation of encoder / decoder with ZIO Schema Deriver-based derivation

Open jdegoes opened this issue 1 year ago • 10 comments

Currently, ZIO JDBC directly converts from Schema to encoders / decoders by generating the appropriate type class instances (JdbcEncoder, JdbcDecoder).

This is not ideal, because it means a user does not have any ability to define a custom encoder / decoder for any part of their ADT.

In order to solve this problem, we can switch from defining Schema => Decoder / Encoder, and instead use the Deriver mechanism built into ZIO Schema, which is designed to solve this precise problem.

Using Deriver to derive JdbcEncoder / JdbcDecoder, we can both automatically support derivation, as well as allow user-defined overrides for parts of a larger data type.

jdegoes avatar Jun 13 '23 12:06 jdegoes

/bounty $250

jdegoes avatar Jun 13 '23 12:06 jdegoes

💎 $250 bounty • ZIO

Steps to solve:

  1. Start working: Comment /attempt #144 with your implementation plan
  2. Submit work: Create a pull request including /claim #144 in the PR body to claim the bounty
  3. Receive payment: 100% of the bounty is received 2-5 days post-reward. Make sure you are eligible for payouts

Additional opportunities:

  • 🔴 Livestream on Algora TV while solving this bounty & earn $200 upon merge! Make sure to have your camera and microphone on. Comment /livestream once live

Thank you for contributing to zio/zio-jdbc!

Add a bountyShare on socials

Attempt Started (GMT+0) Solution
🔴 @FVelasquezM Jun 17, 2023, 2:03:36 AM WIP
🔴 @Andrapyre May 13, 2024, 2:54:25 AM WIP

algora-pbc[bot] avatar Jun 13 '23 12:06 algora-pbc[bot]

Hi, I'd like to try this one /attempt #144

FVelasquezM avatar Jun 17 '23 02:06 FVelasquezM

@FVelasquezM: Reminder that in 7 days the bounty will become up for grabs, so please submit a pull request before then 🙏

algora-pbc[bot] avatar Jun 24 '23 02:06 algora-pbc[bot]

The bounty is up for grabs! Everyone is welcome to /attempt #144 🙌

algora-pbc[bot] avatar Jul 01 '23 02:07 algora-pbc[bot]

I have been playing with this for a few hours but my lack of experience with zio-schema shows and I could not find non-trivial examples of using Deriver. Could I, please, ask someone to help me verify that the following approach could be the right one?

deriveRecord collects values of columns decoded (1) by JdbcDecoders (fields) in Chunk[Any] and then passes them into record.construct to create record (2). Besides that, used column indices are tracked. So far, it reuses existing decoders for primitives.

Thank you.

val drv: Deriver[JdbcDecoder] = new Deriver[JdbcDecoder] {

  override def deriveRecord[A](
    record: Schema.Record[A],
    fields: => Chunk[WrappedF[zio.jdbc.JdbcDecoder, ?]],
    summoned: => Option[JdbcDecoder[A]]
  ): JdbcDecoder[A] =
    new JdbcDecoder[A] {
      override def unsafeDecode(columnIndex: Int, rs: ResultSet): (Int, A) = {
        val (nextColumn, fieldValues) = fields
          .foldLeft((columnIndex, Chunk.empty[Any])) { case ((col, values), decoder) =>
            decoder
              .unwrap
              .decode(col, rs)     // 1
              .fold(
                e => throw new JdbcDecoderError("Cannot decode value", e, rs.getMetaData(), rs.getRow()),
                (c, value) => (c + 1, values :+ value)
              )
          }

        Unsafe
          .unsafe(record.construct(fieldValues))    // 2
          .fold(
            e => throw new JdbcDecoderError("Cannot construct record", new Exception(e), rs.getMetaData(), rs.getRow()),
            a => (nextColumn - 1, a)
          )          
      }
    }
  …
}.autoAcceptSummoned

users:

id name age
1 Pierre 100
2 John 20
3 Nelly 17
case class User(name: String, age: Int)
case class Id(value: Int)
case class Rec(id: Int, user: User, id2: Id)

given Schema[Rec] = DeriveSchema.gen
given JdbcDecoder[Rec] = Derive.derive(drv)

// query
sql"SELECT id, name, age, id * 2 FROM users".query[Rec].selectAll

/*
  Rec(1, User(Pierre, 100), Id(2))
  Rec(2, User(John, 20), Id(4))
  Rec(3, User(Nelly, 17), Id(6))
*/

// with custom decoder of inner record
given JdbcDecoder[Id] = JdbcDecoder.intDecoder.map(n => Id(n + 1000))

/*
  Rec(1, User(Pierre, 100), Id(1002))
  Rec(2, User(John, 20), Id(1004))
  Rec(3, User(Nelly, 17), Id(1006))
*/

jirijakes avatar Jul 09 '23 12:07 jirijakes

My further experiments and attempts to use zio-jdbc in one of my projects bring me to another question: should the codecs work based on position or names? As for now, they seem to work based on names for records (case classes). This has issue with nested case classes (how to name columns in result set and how to refer to nested values). My attempt above uses positional decoding, I think the example I provided would not be possible with name-based decoding.

jirijakes avatar Jul 12 '23 09:07 jirijakes

/attempt #144

Algora profile Completed bounties Tech Active attempts Options
@Andrapyre 1 ZIO bounty
TypeScript, Scala,
Rust & more
zio/zio-json#1055
Cancel attempt

Andrapyre avatar May 13 '24 02:05 Andrapyre

@Andrapyre: Reminder that in 7 days the bounty will become up for grabs, so please submit a pull request before then 🙏

algora-pbc[bot] avatar May 20 '24 02:05 algora-pbc[bot]

The bounty is up for grabs! Everyone is welcome to /attempt #144 🙌

algora-pbc[bot] avatar May 27 '24 02:05 algora-pbc[bot]