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

Rework Read/Write & Fix derivation to use custom instances #2136

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

jatcwang
Copy link
Collaborator

@jatcwang jatcwang commented Nov 13, 2024

Fix both semiauto and automatic derivation to use custom defined Read/Write instances (e.g. in companion objects).

A complete rework of Read and Write is unfortunately necessary because with the previous implementation, we cannot simply obtain a Read[Option[A]] given a Read[A] - we'd need to derive Option[A] from scratch by resolving Read[Option[X]] instances for each of A's columns.

After the rework, both Read and Write are now sealed traits, each with 3 subclasses:

  • Single: Wrapper over a Get/Put
  • SingleOpt: Wrapper over a Get/Put, but is nullable i.e. Read[Option[A]], Write[Option[A]]
  • Composite: A composite of Read/Write instances

Apart from enabling proper semiauto and automatic derivation, the rework also brings these benefits:

  • Derivation rules are much simpler (which also translates to faster compile times). In particular, given a Read/Write[A] we can trivially derive Read/Write[Option[A]].
  • We now correctly handle optionality for Option[CaseClassWithOptionalAndNonOptionalFields]. More tests will be added for this in a follow up PR to demonstrate

Other notes:

  • Semiauto and Auto derivation of unary product type (e.g. 1 element case class) are removed due to it causing auto derivation to pick the wrong path. It seems unnecessary since Write/Read derivation will yield the same behaviour anyway?

Fixes #1054, #2104

@jatcwang jatcwang force-pushed the fix_auto_derivation branch 2 times, most recently from 2076053 to 1453bf1 Compare November 13, 2024 20:46
accum += rr.unsafeGet(rs, idx)
idx += rr.length
}
construct(accum.toList)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If accum is being turned into a list is it more efficient to use ListBuffer instead of ArrayBuffer?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice - that seems to give a 20% boost in the benchmarks :)


}

/** Simple instance wrapping a Put. i.e. single column nullable value */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Put" -> "Get"?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed thanks

implicit def fromGet[A](implicit ev: Get[A]): Read[A] =
new Read(List((ev, NoNulls)), ev.unsafeGetNonNullable) {}
/** Simple instance wrapping a Put. i.e. single column non-null value */
final case class Single[A](get: Get[A]) extends Read[A] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any need for this to be a case class instead of a class? It has no concept of equality and its not data.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I've changed this for for both Read & Write

@@ -121,7 +151,7 @@ class ReadSuite extends munit.FunSuite with ReadSuitePlatform {
// This result doesn't seem ideal, because we should know that Int isn't
// nullable, so the correct result is Some((1, None, 3, None))
// But with how things are wired at the moment this isn't possible
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could remove this comment now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed

Comment on lines +105 to +107
scalacOptions ++= Seq(
"-Wconf:cat=unused-nowarn:s"
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with the global "-Wconf:cat=unused-nowarn:s" setting is that it is applied everywhere, regardless of each particular case.

Another approach that could allow to tackle this on a case-by-case basis is to make use of scalac-compat – it has nowarn213 and nowarn3 which, if applied, would emit @nowarn for Scala 2.13 and Scala 3 respectively, and skip it for Scala 2.12.

AFAIR, scalac-compat is provided automatically by sbt-typelevel.

Copy link
Collaborator Author

@jatcwang jatcwang Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks I didn't know about the scalac-compat. I gave it a go but unfortunately somehow while compiling in Scala 3 it thinks @nowarn213 is unused :/

[warn] -- [E198] Unused Symbol Warning: /home/jacob/proj/contrib/doobie/modules/core/src/test/scala/doobie/util/ReadSuite.scala:14:45 
[warn] 14 |import org.typelevel.scalaccompat.annotation.nowarn213
[warn]    |                                             ^^^^^^^^^
[warn]    |                                             unused import

Despite it being used in

}: @nowarn("msg=.*(u|U)nused import.*")
(I changed nowarn to nowarn213)

@jatcwang jatcwang force-pushed the fix_auto_derivation branch from 1453bf1 to 25dfdd3 Compare November 18, 2024 20:57
@jatcwang jatcwang force-pushed the fix_auto_derivation branch 2 times, most recently from 21e03ed to 0360375 Compare November 26, 2024 21:38
}

/** A Read instance consists of multiple underlying Read instances */
class Composite[A, S0, S1](read0: Read[S0], read1: Read[S1], f: (S0, S1) => A) extends Read[A] {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some benchmarking, I have reworked Composite to be structured more like HLists rather than List[Read[Any]].
With List[Read[Any]] my understanding is that JVM cannot effectively perform inlining because due to the dynamic length/type nature of the list.
But with a HList-like structure (e.g. Composite(Read[Int], Composite(Read[String], Read[Int]))) the call graph and types are "constant" which I think made the JIT happier

Using benchmark from #2139, the current implementation has basically no penalty between Read[A] and Read[Option[A]], whereas the previous implementation has around a 50% performance penalty.

[info] Benchmark                                Mode  Cnt        Score        Error  Units
[info] LargeRow.autoDerivedComplex             thrpt    5   956584.142 ± 105909.280  ops/s
[info] LargeRow.autoDerivedComplexOpt          thrpt    5   925193.390 ± 240656.559  ops/s
[info] LargeRow.rawJdbcComplex                 thrpt    5  1390634.422 ± 195647.819  ops/s
[info] LargeRow.rawJdbcTuple                   thrpt    5  1341330.033 ± 248449.784  ops/s
[info] LargeRow.semiautoDerivedComplex         thrpt    5   957341.195 ± 155499.630  ops/s
[info] LargeRow.semiautoDerivedComplexOpt      thrpt    5   917308.756 ± 181313.503  ops/s
[info] LargeRow.tuple                          thrpt    5  1068742.195 ± 208005.075  ops/s
[info] LargeRow.tupleOpt                       thrpt    5  1072509.030 ± 177644.605  ops/s

@jatcwang jatcwang force-pushed the fix_auto_derivation branch 2 times, most recently from 2b0b963 to 41cb935 Compare December 4, 2024 21:44
Fix both semiauto and automatic derivation to use custom defined Read/Write instances (e.g. in companion objects).

A complete rework of Read and Write is unfortunately necessary because with the previous implementation, we cannot simply derive a `Read[Option[A]]` given a `Read[A]` - we'd need to derive `Option[A]` from scratch by resolving `Read[Option[X]]` instances for each of `A`'s columns.

After the rework, both `Read` and `Write` are now sealed traits, each with 3 subclasses:
- Single: Wrapper over a `Get/Put`
- SingleOpt: Wrapper over a `Get/Put`, but is nullable i.e. `Read[Option[A]]`, `Write[Option[A]]`
- Composite: A composite of `Read/Write` instances

Apart from enabling proper semiauto and automatic derivation, the rework also brings these benefits:

- Derivation rules are much simpler (which also translates to faster compile times). In particular,
 given a `Read/Write[A]` we can trivially derive `Read/Write[Option[A]]`.
- We now correctly handle optionality for `Option[CaseClassWithOptionalAndNonOptionalFields]`. More tests will be added for this in a follow up PR to demonstrate

Other notes:
- Semiauto and Auto derivation of unary product type (e.g. 1 element case class) are removed due to it causing auto derivation to pick the wrong path. It seems unnecessary since
  Write/Read derivation will yield the same behaviour anyway?

Fixes #1054, #2104
@jatcwang jatcwang force-pushed the fix_auto_derivation branch from 41cb935 to 8a83120 Compare December 7, 2024 10:18
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

Successfully merging this pull request may close these issues.

implicit Read[Option[A]] - compilation times
3 participants