Description:
Summary
In Doobie for Scala 3, Read and Write currently support the derives syntax for Product types. However, Get, Put, and Meta lack this capability, creating an inconsistency in the API. This forces users to implement instances manually, which often leads to infinite implicit recursion loops in Scala 3 (#1854).
I propose:
- Implementing derived for Get, Put, and Meta specifically for single-field Product types (newtypes).
- Formally documenting the "Parameter Syntax" as the official solution for opaque type recursion.
1. Proposed Implementation
The implementation follows the established Platform trait pattern and ensures that Doobie's diagnostics (typeStack) are preserved by using tmap and tcontramap with TypeName.
A. Get / GetPlatform
File: modules/core/src/main/scala-3/doobie/util/GetPlatform.scala
// Add to GetPlatform trait
inline def derived[A <: Product](using m: Mirror.ProductOf[A], name: TypeName[A]): Get[A] =
inline erasedValue[m.MirroredElemTypes] match
case _: (inner *: EmptyTuple) =>
val underlying = summonInline[Get[inner]]
// tmap ensures the type name 'A' is pushed to the typeStack for JDBC diagnostics
underlying.tmap(v => m.fromProduct(Tuple1(v)))
case _ =>
compiletime.error("Get derivation is only supported for single-field product types (newtypes).")
B. Put / PutPlatform
File: modules/core/src/main/scala-3/doobie/util/PutPlatform.scala (New file, to be mixed into Put object)
trait PutPlatform:
inline def derived[A <: Product](using m: Mirror.ProductOf[A], name: TypeName[A]): Put[A] =
inline erasedValue[m.MirroredElemTypes] match
case _: (inner *: EmptyTuple) =>
val underlying = summonInline[Put[inner]]
underlying.tcontramap(a => Tuple.fromProductTyped(a).head)
case _ =>
compiletime.error("Put derivation is only supported for single-field product types (newtypes).")
C. Meta
File: modules/core/src/main/scala/doobie/util/meta/meta.scala
// Add to object Meta
inline def derived[A <: Product](using m: Mirror.ProductOf[A], name: TypeName[A]): Meta[A] =
inline erasedValue[m.MirroredElemTypes] match
case _: (inner *: EmptyTuple) =>
val underlying = summonInline[Meta[inner]]
underlying.timap(v => m.fromProduct(Tuple1(v)))(a => Tuple.fromProductTyped(a).head)
case _ =>
compiletime.error("Meta derivation is only supported for single-field product types (newtypes).")
2. Why priority traits are not needed
Unlike Read and Write, which support automatic derivation (and thus require LowestPriority to avoid shadowing user-defined instances), this proposal for Get, Put, and Meta focuses on semi-automatic derivation (explicit opt-in via derives).
Since the instance is only generated when explicitly requested, we don't need the Derived wrapper or LowestPriority indirection. If a user defines a manual given and also uses derives, Scala 3 will correctly report an ambiguity error.
3. Addressing Opaque Type Recursion (#1854)
Since opaque types do not generate a Mirror automatically, derives will not solve their recursion directly. I will contribute a section to The Book of Doobie documenting the "Parameter Syntax" as the recommended pattern to break the self-reference loop:
opaque type UserId = Int
object UserId {
// Safe: requesting Get[Int] as an explicit parameter prevents the
// compiler from picking given_Get_UserId recursively.
given (using g: Get[Int]): Get[UserId] = g.tmap(UserId(_))
}
Benefits
API Parity: Enables the clean derives Get, Put, Meta syntax, matching the UX of Read and Write.
Diagnostics: High-quality JDBC error messages are maintained because TypeName[A] is used to populate the typeStack.
Type Safety: Uses Scala 3 Tuple.fromProductTyped for safe, macro-free extraction of field values.
Simplicity: The implementation is direct and lightweight, avoiding unnecessary indirection since there is no global automatic derivation for these types.
Education: Directly resolves the long-standing confusion in #1854 by providing a clear, documented solution for opaque types.
Example usage after these changes:
case class MessageId(value: UUID) derives Get, Put, Meta
Description:
Summary
In Doobie for Scala 3, Read and Write currently support the derives syntax for Product types. However, Get, Put, and Meta lack this capability, creating an inconsistency in the API. This forces users to implement instances manually, which often leads to infinite implicit recursion loops in Scala 3 (#1854).
I propose:
1. Proposed Implementation
The implementation follows the established Platform trait pattern and ensures that Doobie's diagnostics (typeStack) are preserved by using tmap and tcontramap with TypeName.
A. Get / GetPlatform
File: modules/core/src/main/scala-3/doobie/util/GetPlatform.scala
B. Put / PutPlatform
File: modules/core/src/main/scala-3/doobie/util/PutPlatform.scala (New file, to be mixed into Put object)
C. Meta
File: modules/core/src/main/scala/doobie/util/meta/meta.scala
2. Why priority traits are not needed
Unlike Read and Write, which support automatic derivation (and thus require LowestPriority to avoid shadowing user-defined instances), this proposal for Get, Put, and Meta focuses on semi-automatic derivation (explicit opt-in via derives).
Since the instance is only generated when explicitly requested, we don't need the Derived wrapper or LowestPriority indirection. If a user defines a manual given and also uses derives, Scala 3 will correctly report an ambiguity error.
3. Addressing Opaque Type Recursion (#1854)
Since opaque types do not generate a Mirror automatically, derives will not solve their recursion directly. I will contribute a section to The Book of Doobie documenting the "Parameter Syntax" as the recommended pattern to break the self-reference loop:
Benefits
API Parity: Enables the clean derives Get, Put, Meta syntax, matching the UX of Read and Write.
Diagnostics: High-quality JDBC error messages are maintained because TypeName[A] is used to populate the typeStack.
Type Safety: Uses Scala 3 Tuple.fromProductTyped for safe, macro-free extraction of field values.
Simplicity: The implementation is direct and lightweight, avoiding unnecessary indirection since there is no global automatic derivation for these types.
Education: Directly resolves the long-standing confusion in #1854 by providing a clear, documented solution for opaque types.
Example usage after these changes: