zio / zio-jdbc

A small, idiomatic ZIO interface to JDBC.
Apache License 2.0
81 stars 61 forks source link

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

Open jdegoes opened 1 year ago

jdegoes commented 1 year ago

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 commented 1 year ago

/bounty $250

algora-pbc[bot] commented 1 year ago

💎 $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:

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
FVelasquezM commented 1 year ago

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

algora-pbc[bot] commented 1 year ago

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

algora-pbc[bot] commented 1 year ago

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

jirijakes commented 1 year ago

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 commented 1 year ago

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.

Andrapyre commented 2 months ago

/attempt #144

Algora profile Completed bounties Tech Active attempts Options
@Andrapyre 1 ZIO bounty
TypeScript, Scala,
Rust & more
zio/zio-json#1055
Cancel attempt
algora-pbc[bot] commented 2 months ago

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

algora-pbc[bot] commented 1 month ago

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