All Downloads are FREE. Search and download functionalities are using the official Maven repository.

doobie.postgres.Text.scala Maven / Gradle / Ivy

// Copyright (c) 2013-2020 Rob Norris and Contributors
// This software is licensed under the MIT License (MIT).
// For more information see LICENSE or https://opensource.org/licenses/MIT

package doobie.postgres

import cats.{ContravariantSemigroupal, Foldable}
import cats.syntax.foldable.*

/** Typeclass for types that can be written as Postgres literal text, using the default DELIMETER and NULL values, for
  * use with `COPY`. If you wish to implement an instance it's worth reading the documentation at the link below.
  * @see
  *   [[https://www.postgresql.org/docs/9.6/static/sql-copy.html Postgres `COPY` command]]
  */

trait Text[A] { outer =>

  /** Construct an encoder for `A` that appends to the provided `StringBuilder.
    * @param a
    *   the value to encode
    */
  def unsafeEncode(a: A, sb: StringBuilder): Unit

  /** Variant of unsafeEncode, called for array members which require more escaping. Same as unsafeEncode unless
    * overridden.
    */
  def unsafeArrayEncode(a: A, sb: StringBuilder): Unit =
    unsafeEncode(a, sb)

  /** Encode `a`. */
  final def encode(a: A): String = {
    val sb = new StringBuilder
    unsafeEncode(a, sb)
    sb.toString
  }

  /** `Text` is a contravariant functor. */
  final def contramap[B](f: B => A): Text[B] =
    Text.instance((b, sb) => outer.unsafeEncode(f(b), sb))

  /** `Text` is semigroupal. */
  def product[B](fb: Text[B]): Text[(A, B)] =
    new Text[(A, B)] {
      def unsafeEncode(ab: (A, B), sb: StringBuilder) = {
        outer.unsafeEncode(ab._1, sb)
        sb.append(Text.DELIMETER)
        fb.unsafeEncode(ab._2, sb)
      }
    }

}
object Text extends TextInstances with TextPlatform {
  def apply[A](implicit ev: Text[A]): ev.type = ev

  val DELIMETER: Char = '\t'
  val NULL: String = "\\N"

  /** Construct an instance, given a function matching the `unsafeEncode` signature.
    * @param f
    *   a function from `(A, DELIMETER, NULL) => StringBuilder => StringBuilder`
    */
  def instance[A](f: (A, StringBuilder) => Unit): Text[A] =
    new Text[A] {
      def unsafeEncode(a: A, sb: StringBuilder) = f(a, sb)
    }

}

trait TextInstances extends TextInstances0 { this: Text.type =>

  /** `Text` is both contravariant and semigroupal. */
  implicit val CsvContravariantSemigroupal: ContravariantSemigroupal[Text] =
    new ContravariantSemigroupal[Text] {
      def contramap[A, B](fa: Text[A])(f: B => A) = fa.contramap(f)
      def product[A, B](fa: Text[A], fb: Text[B]): Text[(A, B)] = fa.product(fb)
    }

  // String encoder escapes any embedded `QUOTE` characters.
  implicit val stringInstance: Text[String] =
    new Text[String] {

      // Standard char encodings that don't differ in array context
      def stdChar(c: Char, sb: StringBuilder): StringBuilder =
        c match {
          case '\b' => sb.append("\\b")
          case '\f' => sb.append("\\f")
          case '\n' => sb.append("\\n")
          case '\r' => sb.append("\\r")
          case '\t' => sb.append("\\t")
          case 0x0b => sb.append("\\v")
          case c    => sb.append(c.toChar)
        }

      def unsafeEncode(s: String, sb: StringBuilder) =
        s.foreach {
          case '\\' => sb.append("\\\\") // backslash must be doubled
          case c    => stdChar(c, sb)
        }

      // I am not confident about this encoder. Postgres seems not to be able to cope with low
      // control characters or high whitespace characters so these are simply filtered out in the
      // tests. It should accommodate arrays of non-pathological strings but it would be nice to
      // have a complete specification of what's actually happening.
      override def unsafeArrayEncode(s: String, sb: StringBuilder) = {
        sb.append('"')
        s.foreach {
          case '\"' => sb.append("\\\\\"")
          case '\\' => sb.append("\\\\\\\\") // srsly
          case c    => stdChar(c, sb)
        }
        sb.append('"')
        ()
      }
    }

  // Char
  implicit val charInstance: Text[Char] = instance((n, sb) => { sb.append(n.toString); () })

  // Primitive Numerics
  implicit val intInstance: Text[Int] = instance((n, sb) => { sb.append(n); () })
  implicit val shortInstance: Text[Short] = instance((n, sb) => { sb.append(n); () })
  implicit val longInstance: Text[Long] = instance((n, sb) => { sb.append(n); () })
  implicit val floatInstance: Text[Float] = instance((n, sb) => { sb.append(n); () })
  implicit val doubleInstance: Text[Double] = instance((n, sb) => { sb.append(n); () })

  // Big Numerics
  implicit val bigDecimalInstance: Text[BigDecimal] = instance { (n, sb) => { sb.append(n.toString); () } }

  // Boolean
  implicit val booleanInstance: Text[Boolean] =
    instance((b, sb) => { sb.append(b); () })

  // Date, Time, etc.

  // Byte arrays in \\x01A3DD.. format.
  implicit val byteArrayInstance: Text[Array[Byte]] =
    instance { (bs, sb) =>
      sb.append("\\\\x")
      if (bs.length > 0) {
        val hex = BigInt(1, bs).toString(16)
        val pad = bs.length * 2 - hex.length
        (0 until pad).foreach(_ => sb.append("0"))
        sb.append(hex)
        ()
      }
    }

  // Any non-option Text can be lifted to Option
  implicit def option[A](
      implicit csv: Text[A]
  ): Text[Option[A]] =
    instance {
      case (Some(a), sb) => { csv.unsafeEncode(a, sb); () }
      case (None, sb)    => { sb.append(Text.NULL); () }
    }

}

trait TextInstances0 extends TextInstances1 { this: Text.type =>

  // Iterable and views thereof, as [nested] ARRAY
  implicit def iterableInstance[F[_], A](
      implicit
      ev: Text[A],
      f: F[A] => Iterable[A]
  ): Text[F[A]] =
    instance { (fa, sb) =>
      var first = true
      sb.append("{")
      f(fa).foreach { a =>
        if (first) first = false
        else sb.append(',')
        ev.unsafeArrayEncode(a, sb)
      }
      sb.append('}')
      ()
    }

}

trait TextInstances1 { this: Text.type =>

  // Foldable, not as fast
  implicit def foldableInstance[F[_]: Foldable, A](
      implicit ev: Text[A]
  ): Text[F[A]] =
    iterableInstance[List, A].contramap(_.toList)

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy