typelevel / typelevel/skunk

Add support for composite type and record type

Open
#578 0 comments 8 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Scala
Stars
1.7k
Forks
175
Avg merge
5d 14h
Merged PRs (30d)
9

Description

I currently use the following helpers in my own code to support these types. records are simply composite type without predefined structure.

import scala.util.matching.Regex

import cats.data.State
import cats.syntax.all.*
import skunk.codec.all.*
import skunk.data.{Arr, Type}
import skunk.implicits.*
import skunk.syntax.*
import skunk.{Codec, Decoder}

import CompositeTypeCodec.*

enum CompositeType:
  case Record
  case Custom(name: String)

object CompositeTypeCodec:
  private def unparameterize(name: String): String =
    name match
      case s"$_name($n)" => _name
      case _             => name

  def mkComposite[A <: Product](codec: Codec[A], desc: CompositeType = CompositeType.Record): Codec[A] =
    new Codec[A]:
      override def encode(a: A): List[Option[String]] =
        val elem = codec.encode(a).flattenOption.mkString("(", ",", ")")
        List(Some(elem))

      override val sql: State[Int, String] =
        codec.sql.map(a => s"($a)")

      override val types: List[Type] =
        desc match
          case CompositeType.Record =>
            List(Type.record)
          case CompositeType.Custom(name) =>
            // parameterized types must be unparameterized
            val types = codec.types.map(ty => ty.copy(name = unparameterize(ty.name)))
            List(Type(name, types))

      override def decode(offset: Int, s: List[Option[String]]): Either[Decoder.Error, A] =
        s match
          case Some(s"($inner)") :: Nil =>
            val composites = inner.split(",").map(_.some).toList
            codec.decode(offset, composites)
          case None :: Nil =>
            Left(Decoder.Error(offset, 1, s"Unexpected NULL value in non-optional column."))
          case _ =>
            Left(Decoder.Error(offset, 1, s"Expected one input value to decode, got ${s.length}."))

extension [A <: Product](self: Codec[A]) 
  def asArr: Codec[Arr[A]] =
    val ty     = Type(s"_${self.types.head.name}", self.types)
    val encode = (elem: A) => self.encode(elem).head.get
    val decode = (str: String) => self.decode(0, List(Some(str))).left.map(_.message)
    Codec.array(encode, decode, ty)

Usage

case class Point(x: BigDecimal, y: BigDecimal)

val pointArrCodec: Codec[Arr[Point]] = CompositeTypeCodec
  .mkComposite(numeric *: numeric, CompositeType.Custom("point"))
  .imap(Point.apply)(point => point.x -> point.y)
  .asArr

// `create type book_id as(id_part varchar(5), category varchar(5));`
case class BookId(id: String, category: String)

val c_bookId: Codec[BookId] =
  CompositeTypeCodec
    .mkComposite(varchar(5) *: varchar(5), CompositeType.Custom("book_id"))
    .imap(BookId.apply)(bookId => bookId.id -> bookId.category)

EDIT (2022/07/04):
There is currently an issue if the composite type contains a string though. Probably easy to fix.
Fixed and tested with varchar(5). Parameterised type need to be unparameterized.

Probably needs more testing with types that can be parameterized, i.e char(5)/char, byte(1), etc.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Begin with the existing Codec, Type, Decoder, and Codec.array APIs used by the supplied CompositeTypeCodec example; trace how PostgreSQL composite and record values are represented and how parameterized types are named. Define completion as support matching the shown custom and record usages, including array handling, NULL/error behavior, and tests for parameterized fields such as varchar(5), char, and byte(1).

Written by the indexing model from the issue text.

Assessment

Tech stack
postgresql, scala
Domain
database
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.