doobie.postgres.Instances.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of doobie-postgres_2.12 Show documentation
Show all versions of doobie-postgres_2.12 Show documentation
Postgres support for doobie.
The newest version!
// 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 doobie.enumerated.JdbcType
import doobie.*
import doobie.util.invariant.*
import java.util.{UUID, Map as JMap}
import java.net.InetAddress
import org.postgresql.util.*
import org.postgresql.geometric.*
import scala.reflect.ClassTag
import cats.data.NonEmptyList.{of as NonEmptyListOf}
import org.tpolecat.typename.*
trait Instances {
// N.B. `Meta` is the lowest-level mapping and must always cope with NULL. Easy to forget.
// Geometric Types, minus PGline which is "not fully implemented"
implicit val PGboxType: Meta[PGbox] = Meta.Advanced.other[PGbox]("box")
implicit val PGcircleType: Meta[PGcircle] = Meta.Advanced.other[PGcircle]("circle")
implicit val PGlsegType: Meta[PGlseg] = Meta.Advanced.other[PGlseg]("lseg")
implicit val PGpathType: Meta[PGpath] = Meta.Advanced.other[PGpath]("path")
implicit val PGpointType: Meta[PGpoint] = Meta.Advanced.other[PGpoint]("point")
implicit val PGpolygonType: Meta[PGpolygon] = Meta.Advanced.other[PGpolygon]("polygon")
// PGmoney doesn't seem to work:
// PSQLException: : Bad value for type double : 1,234.56 (AbstractJdbc2ResultSet.java:3059)
// org.postgresql.jdbc2.AbstractJdbc2ResultSet.toDouble(AbstractJdbc2ResultSet.java:3059)
// org.postgresql.jdbc2.AbstractJdbc2ResultSet.getDouble(AbstractJdbc2ResultSet.java:2383)
// org.postgresql.jdbc2.AbstractJdbc2ResultSet.internalGetObject(AbstractJdbc2ResultSet.java:152)
// org.postgresql.jdbc3.AbstractJdbc3ResultSet.internalGetObject(AbstractJdbc3ResultSet.java:36)
// org.postgresql.jdbc4.AbstractJdbc4ResultSet.internalGetObject(AbstractJdbc4ResultSet.java:300)
// org.postgresql.jdbc2.AbstractJdbc2ResultSet.getObject(AbstractJdbc2ResultSet.java:2704)
// Interval Type
// There is no natural mapping to java.time types (https://github.com/tpolecat/doobie/pull/315)
// so we provide the bare mapping and leave it at that.
implicit val PGIntervalType: Meta[PGInterval] = Meta.Advanced.other[PGInterval]("interval")
// UUID
implicit val UuidType: Meta[UUID] = Meta.Advanced.other[UUID]("uuid")
// Network Address Types
implicit val InetType: Meta[InetAddress] = Meta.Advanced.other[PGobject]("inet").timap[InetAddress](o =>
Option(o).map(a => InetAddress.getByName(a.getValue)).orNull)(a =>
Option(a).map { a =>
val o = new PGobject
o.setType("inet")
o.setValue(a.getHostAddress)
o
}.orNull)
// java.sql.Array::getArray returns an Object that may be of primitive type or of boxed type,
// depending on the driver, so we can't really abstract over it. Also there's no telling what
// happens with multi-dimensional arrays since most databases don't support them. So anyway here
// we go with PostgreSQL support:
//
// PostgreSQL arrays show up as Array[AnyRef] with `null` for NULL, so that's mostly sensible;
// there would be no way to distinguish 0 from NULL otherwise for an int[], for example. So,
// these arrays can be multi-dimensional and can have NULL cells, but cannot have NULL slices;
// i.e., {{1,2,3}, {4,5,NULL}} is ok but {{1,2,3}, NULL} is not. So this means we only have to
// worry about Array[Array[...[A]]] and Array[Array[...[Option[A]]]] in our mappings.
// Construct a pair of Meta instances for arrays of lifted (nullable) and unlifted (non-
// nullable) reference types (as noted above, PostgreSQL doesn't ship arrays of primitives). The
// automatic lifting to Meta will give us lifted and unlifted arrays, for a total of four variants
// of each 1-d array type. In the non-nullable case we simply check for nulls and perform a cast;
// in the nullable case we must copy the array in both directions to lift/unlift Option.
private def boxedPair[A >: Null <: AnyRef: ClassTag](
elemType: String,
arrayType: String,
arrayTypeT: String*
): (Meta[Array[A]], Meta[Array[Option[A]]]) = {
val raw = Meta.Advanced.array[A](elemType, arrayType, arrayTypeT*)
// Ensure `a`, which may be null, which is ok, contains no null elements.
def checkNull[B >: Null](a: Array[B], e: Exception): Array[B] =
if (a == null) null else if (a.exists(_ == null)) throw e else a
(
raw.timap(checkNull(_, NullableCellRead))(checkNull(_, NullableCellUpdate)),
raw.timap[Array[Option[A]]](_.map(Option(_)))(_.map(_.orNull).toArray))
}
// Arrays of lifted (nullable) and unlifted (non-nullable) Java wrapped primitives. PostgreSQL
// does not seem to support tinyint[](use a bytea instead) and smallint[] always arrives as Int[]
// so you can xmap if you need Short[]. The type names provided here are what is reported by JDBC
// when metadata is requested; there are numerous aliases but these are the ones we need. Nothing
// about this is portable, sorry. (╯°□°)╯︵ ┻━┻
private val boxedPairBoolean = boxedPair[java.lang.Boolean]("bit", "_bit")
implicit val unliftedBooleanArrayType: Meta[Array[java.lang.Boolean]] = boxedPairBoolean._1
implicit val liftedBooleanArrayType: Meta[Array[Option[java.lang.Boolean]]] = boxedPairBoolean._2
private val boxedPairInteger = boxedPair[java.lang.Integer]("int4", "_int4")
implicit val unliftedIntegerArrayType: Meta[Array[java.lang.Integer]] = boxedPairInteger._1
implicit val liftedIntegerArrayType: Meta[Array[Option[java.lang.Integer]]] = boxedPairInteger._2
private val boxedPairLong = boxedPair[java.lang.Long]("int8", "_int8")
implicit val unliftedLongArrayType: Meta[Array[java.lang.Long]] = boxedPairLong._1
implicit val liftedLongArrayType: Meta[Array[Option[java.lang.Long]]] = boxedPairLong._2
private val boxedPairFloat = boxedPair[java.lang.Float]("float4", "_float4")
implicit val unliftedFloatArrayType: Meta[Array[java.lang.Float]] = boxedPairFloat._1
implicit val liftedFloatArrayType: Meta[Array[Option[java.lang.Float]]] = boxedPairFloat._2
private val boxedPairDouble = boxedPair[java.lang.Double]("float8", "_float8")
implicit val unliftedDoubleArrayType: Meta[Array[java.lang.Double]] = boxedPairDouble._1
implicit val liftedDoubleArrayType: Meta[Array[Option[java.lang.Double]]] = boxedPairDouble._2
private val boxedPairString = boxedPair[java.lang.String]("varchar", "_varchar", "_char", "_text", "_bpchar")
implicit val unliftedStringArrayType: Meta[Array[java.lang.String]] = boxedPairString._1
implicit val liftedStringArrayType: Meta[Array[Option[java.lang.String]]] = boxedPairString._2
private val boxedPairUUID = boxedPair[java.util.UUID]("uuid", "_uuid")
implicit val unliftedUUIDArrayType: Meta[Array[java.util.UUID]] = boxedPairUUID._1
implicit val liftedUUIDArrayType: Meta[Array[Option[java.util.UUID]]] = boxedPairUUID._2
private val boxedPairBigDecimal = boxedPair[java.math.BigDecimal]("numeric", "_decimal", "_numeric")
implicit val unliftedBigDecimalArrayType: Meta[Array[java.math.BigDecimal]] = boxedPairBigDecimal._1
implicit val iftedBigDecimalArrayType: Meta[Array[Option[java.math.BigDecimal]]] = boxedPairBigDecimal._2
// Unboxed equivalents (actually identical in the lifted case). We require that B is the unboxed
// equivalent of A, otherwise this will fail in spectacular fashion, and we're using a cast in the
// lifted case because the representation is identical, assuming no nulls. In the long run this
// may need to become something slower but safer. Unclear.
private def unboxedPair[A >: Null <: AnyRef: ClassTag, B <: AnyVal: ClassTag](f: A => B, g: B => A)(
implicit
boxed: Meta[Array[A]],
boxedLifted: Meta[Array[Option[A]]]
): (Meta[Array[B]], Meta[Array[Option[B]]]) =
// TODO: assert, somehow, that A is the boxed version of B so we catch errors on instance
// construction, which is somewhat better than at [logical] execution time.
(
boxed.timap(a => if (a == null) null else a.map(f))(a => if (a == null) null else a.map(g)),
boxedLifted.timap(_.asInstanceOf[Array[Option[B]]])(_.asInstanceOf[Array[Option[A]]]))
// Arrays of lifted (nullable) and unlifted (non-nullable) AnyVals
private val unboxedPairBoolean =
unboxedPair[java.lang.Boolean, scala.Boolean](_.booleanValue, java.lang.Boolean.valueOf)
implicit val unliftedUnboxedBooleanArrayType: Meta[Array[scala.Boolean]] = unboxedPairBoolean._1
implicit val liftedUnboxedBooleanArrayType: Meta[Array[Option[scala.Boolean]]] = unboxedPairBoolean._2
private val unboxedPairInteger = unboxedPair[java.lang.Integer, scala.Int](_.intValue, java.lang.Integer.valueOf)
implicit val unliftedUnboxedIntegerArrayType: Meta[Array[scala.Int]] = unboxedPairInteger._1
implicit val liftedUnboxedIntegerArrayType: Meta[Array[Option[scala.Int]]] = unboxedPairInteger._2
private val unboxedPairLong = unboxedPair[java.lang.Long, scala.Long](_.longValue, java.lang.Long.valueOf)
implicit val unliftedUnboxedLongArrayType: Meta[Array[scala.Long]] = unboxedPairLong._1
implicit val liftedUnboxedLongArrayType: Meta[Array[Option[scala.Long]]] = unboxedPairLong._2
private val unboxedPairFloat = unboxedPair[java.lang.Float, scala.Float](_.floatValue, java.lang.Float.valueOf)
implicit val unliftedUnboxedFloatArrayType: Meta[Array[scala.Float]] = unboxedPairFloat._1
implicit val liftedUnboxedFloatArrayType: Meta[Array[Option[scala.Float]]] = unboxedPairFloat._2
private val unboxedPairDouble = unboxedPair[java.lang.Double, scala.Double](_.doubleValue, java.lang.Double.valueOf)
implicit val unliftedUnboxedDoubleArrayType: Meta[Array[scala.Double]] = unboxedPairDouble._1
implicit val liftedUnboxedDoubleArrayType: Meta[Array[Option[scala.Double]]] = unboxedPairDouble._2
// Arrays of scala.BigDecimal - special case as BigDecimal can be null
implicit val bigDecimalMeta: Meta[Array[BigDecimal]] = Meta[Array[java.math.BigDecimal]]
.timap(_.map(a => if (a == null) null else BigDecimal.apply(a)))(_.map(a => if (a == null) null else a.bigDecimal))
implicit val optionBigDecimalMeta: Meta[Array[Option[BigDecimal]]] = Meta[Array[Option[java.math.BigDecimal]]]
.timap(_.map(_.map(a => if (a == null) null else BigDecimal.apply(a))))(_.map(_.map(a =>
if (a == null) null else a.bigDecimal)))
// So, it turns out that arrays of structs don't work because something is missing from the
// implementation. So this means we will only be able to support primitive types for arrays.
//
// java.sql.SQLFeatureNotSupportedException: Method org.postgresql.jdbc4.Jdbc4Array.getArrayImpl(long,int,Map) is not yet implemented.
// at org.postgresql.Driver.notImplemented(Driver.java:729)
// at org.postgresql.jdbc2.AbstractJdbc2Array.buildArray(AbstractJdbc2Array.java:771)
// at org.postgresql.jdbc2.AbstractJdbc2Array.getArrayImpl(AbstractJdbc2Array.java:171)
// at org.postgresql.jdbc2.AbstractJdbc2Array.getArray(AbstractJdbc2Array.java:128)
// TODO: multidimensional arrays; in the worst case it's just copy/paste of everything above but
// we can certainly do better than that.
private def enumPartialMeta(name: String): Meta[String] =
Meta.Basic.many[String](
NonEmptyListOf(JdbcType.Other, JdbcType.VarChar), // https://github.com/tpolecat/doobie/issues/303
NonEmptyListOf(JdbcType.Other, JdbcType.VarChar),
Nil,
(rs, n) => rs.getString(n),
(ps, n, a) => {
val o = new PGobject
o.setValue(a.toString)
o.setType(name)
ps.setObject(n, o)
},
(rs, n, a) => {
val o = new PGobject
o.setValue(a.toString)
o.setType(name)
rs.updateObject(n, o)
}
)
/** Construct a `Meta` for values of the given type, mapped via `String` to the named PostgreSQL enum type.
*/
def pgEnumString[A: TypeName](name: String, f: String => A, g: A => String): Meta[A] =
enumPartialMeta(name).timap[A](f)(g)
/** Construct a `Meta` for values of the given type, mapped via `String` to the named PostgreSQL enum type with
* tranparent partiality.
*/
@SuppressWarnings(Array("org.wartremover.warts.Throw"))
def pgEnumStringOpt[A: TypeName](name: String, f: String => Option[A], g: A => String): Meta[A] =
pgEnumString(name, { (s: String) => f(s).getOrElse(throw doobie.util.invariant.InvalidEnum[A](s)) }, g)
/** Construct a `Meta` for value members of the given `Enumeration`.
*/
def pgEnum(e: Enumeration, name: String): Meta[e.Value] =
pgEnumString[e.Value](
name,
a =>
try e.withName(a)
catch {
case _: NoSuchElementException => throw InvalidEnum[e.Value](a)
},
_.toString)
/** Construct a `Meta` for value members of the given Java `enum`.
*/
def pgJavaEnum[E <: java.lang.Enum[E]: TypeName](name: String)(implicit E: ClassTag[E]): Meta[E] = {
val clazz = E.runtimeClass.asInstanceOf[Class[E]]
pgEnumString[E](
name,
a =>
try java.lang.Enum.valueOf(clazz, a)
catch {
case _: IllegalArgumentException => throw InvalidEnum[E](a)
},
_.name)
}
/** HSTORE maps to a java.util.Map[String, String]. */
implicit val hstoreMetaJava: Meta[JMap[String, String]] =
Meta.Advanced.other[JMap[String, String]]("hstore")
/** HSTORE maps to a Map[String, String]. */
implicit val hstoreMeta: Meta[Map[String, String]] =
hstoreMetaJava.timap[Map[String, String]](doobie.util.compat.mapToScala(_).toMap)(doobie.util.compat.scalaToMap(_))
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy