Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions modules/bench/src/main/scala/doobie/bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ d.b.t.copy_stream avgt 2 382.481 ns/op
# JMH version: 1.37
# VM version: JDK 21.0.8, OpenJDK 64-Bit Server VM
Benchmark Mode Cnt Score Error Units
d.b.i.array_512 ss 1500 2108.146 ± 29.363 ns/op
d.b.i.array_1024 ss 1500 1282.125 ± 17.193 ns/op
d.b.i.array_2048 ss 1500 901.204 ± 27.426 ns/op
d.b.i.batch_512 ss 1500 3610.360 ± 49.533 ns/op
d.b.i.batch_1024 ss 1500 2824.224 ± 158.971 ns/op
d.b.i.batch_2048 ss 1500 2413.790 ± 15.661 ns/op
d.b.i.values_512 ss 1500 2661.017 ± 70.472 ns/op
d.b.i.values_1024 ss 1500 1930.406 ± 25.466 ns/op
d.b.i.values_2048 ss 1500 1759.005 ± 13.514 ns/op
d.b.i.array_512 ss 1500 2637.671 ± 74.508 ns/op
d.b.i.array_1024 ss 1500 1536.033 ± 17.970 ns/op
d.b.i.array_2048 ss 1500 1115.185 ± 20.325 ns/op
d.b.i.batch_512 ss 1500 3894.445 ± 42.689 ns/op
d.b.i.batch_1024 ss 1500 3040.211 ± 21.192 ns/op
d.b.i.batch_2048 ss 1500 2648.115 ± 19.796 ns/op
d.b.i.values_512 ss 1500 3057.419 ± 34.115 ns/op
d.b.i.values_1024 ss 1500 2360.481 ± 41.843 ns/op
d.b.i.values_2048 ss 1500 2343.947 ± 116.611 ns/op
```
32 changes: 20 additions & 12 deletions modules/bench/src/main/scala/doobie/bench/insert.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import doobie.util.Write
import doobie.util.update.Update
import org.openjdk.jmh.annotations.*

import java.time.LocalDate
import java.util.concurrent.TimeUnit

@BenchmarkMode(Array(Mode.SingleShotTime))
Expand Down Expand Up @@ -70,50 +71,56 @@ class insert {
object insert {
import doobie.postgres.instances.array.*

final case class Widget(name: String, extensions: Int)
final case class Widget(name: String, extensions: Int, produced: LocalDate)
object Widget {
implicit val write: Write[Widget] = Write.derived

def generate(n: Int) = List.fill(n)(Widget("widget", n))
private val now = LocalDate.now()

def generate(n: Int) = Vector.fill(n)(Widget("widget", n, now.plusDays(n.toLong)))
}

private val insertBatch = {
val sql = """
INSERT INTO bench_widget
(name, extensions)
(name, extensions, produced)
VALUES
(?, ?)
(?, ?, ?)
"""
Update[Widget](sql)
}

private def insertValues(widgets: List[Widget]) = {
private def insertValues(widgets: Vector[Widget]) = {
val sql = fr"""
INSERT INTO bench_widget
(name, extensions)
(name, extensions, produced)
VALUES
${widgets.map(w => fr"(${w.name}, ${w.extensions})").intercalate(fr",")}
${widgets.map(w => fr"(${w.name}, ${w.extensions}, ${w.produced})").intercalate(fr",")}
"""
sql.update
}

private def insertArrayValues(widgets: List[Widget]) = {
private def insertArrayValues(widgets: Vector[Widget]) = {
val n = widgets.length
val names = Array.ofDim[String](n)
val extensions = Array.ofDim[Int](n)
val produced = Array.ofDim[LocalDate](n)
var i = 0
widgets.foreach { w =>
while (i < n) {
val w = widgets(i)
names(i) = w.name
extensions(i) = w.extensions
produced(i) = w.produced
i = i + 1
}

val sql = fr"""
INSERT INTO bench_widget
(name, extensions)
(name, extensions, produced)
SELECT * FROM unnest(
$names::text[],
$extensions::int4[]
$extensions::int4[],
$produced::date[]
)
"""
sql.update
Expand All @@ -123,7 +130,8 @@ object insert {
sql"drop table if exists bench_widget".update.run *>
sql"""create table bench_widget (
name text not null,
extensions integer not null
extensions integer not null,
produced date not null
)""".update.run.void

@State(Scope.Thread)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ package doobie.postgres.instances
import doobie.util.invariant.*
import doobie.util.meta.Meta

import java.time.LocalDate
import scala.reflect.ClassTag

@SuppressWarnings(Array("org.wartremover.warts.AutoUnboxing"))
object array {

// java.sql.Array::getArray returns an Object that may be of primitive type or of boxed type,
Expand All @@ -27,32 +29,40 @@ object array {
// 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.
@SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.Throw"))
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),
)
boxedPairMeta(raw)
}

// 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. (╯°□°)╯︵ ┻━┻
@SuppressWarnings(Array("org.wartremover.warts.Null"))
private def boxedPairMeta[A >: Null <: AnyRef: ClassTag](
raw: Meta[Array[A]],
): (Meta[Array[A]], Meta[Array[Option[A]]]) = (
raw.timap(checkNull(_, NullableCellRead()))(checkNull(_, NullableCellUpdate())),
raw.timap[Array[Option[A]]](_.map(Option(_)))(_.map(_.orNull).toArray),
)

// Ensure `a`, which may be null, which is ok, contains no null elements.
@SuppressWarnings(Array("org.wartremover.warts.Null", "org.wartremover.warts.Throw"))
private def checkNull[B >: Null](a: Array[B], e: => Exception): Array[B] =
if (a == null) null else if (a.exists(_ == null)) throw e else a

// Arrays of lifted (nullable) and unlifted (non-nullable) Java wrapped primitives.
// 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.

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 boxedPairShort = boxedPair[java.lang.Short]("int2", "_int2")
implicit val unliftedShortArrayType: Meta[Array[java.lang.Short]] = boxedPairShort._1
implicit val liftedShortArrayType: Meta[Array[Option[java.lang.Short]]] = boxedPairShort._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
Expand Down Expand Up @@ -81,6 +91,19 @@ object array {
implicit val unliftedBigDecimalArrayType: Meta[Array[java.math.BigDecimal]] = boxedPairBigDecimal._1
implicit val iftedBigDecimalArrayType: Meta[Array[Option[java.math.BigDecimal]]] = boxedPairBigDecimal._2

private val boxedPairDate = boxedPair[java.sql.Date]("date", "_date")
implicit val unliftedDateArrayType: Meta[Array[java.sql.Date]] = boxedPairDate._1
implicit val liftedDateArrayType: Meta[Array[Option[java.sql.Date]]] = boxedPairDate._2

implicit val unliftedLocalDateArrayType: Meta[Array[LocalDate]] =
unliftedDateArrayType.timap(_.map(_.toLocalDate))(_.map(java.sql.Date.valueOf))
implicit val liftedLocalDateArrayType: Meta[Array[Option[LocalDate]]] =
liftedDateArrayType.timap(_.map(_.map(_.toLocalDate)))(_.map(_.map(java.sql.Date.valueOf)))

private val boxedPairTimestamp = boxedPair[java.sql.Timestamp]("timestamp", "_timestamp")
implicit val unliftedTimestampArrayType: Meta[Array[java.sql.Timestamp]] = boxedPairTimestamp._1
implicit val liftedTimestampArrayType: Meta[Array[Option[java.sql.Timestamp]]] = boxedPairTimestamp._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
Expand All @@ -91,31 +114,33 @@ object array {
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, Boolean](_.booleanValue, java.lang.Boolean.valueOf)
private val unboxedPairBoolean = unboxedPair[java.lang.Boolean, Boolean](Boolean2boolean, boolean2Boolean)
implicit val unliftedUnboxedBooleanArrayType: Meta[Array[Boolean]] = unboxedPairBoolean._1
implicit val liftedUnboxedBooleanArrayType: Meta[Array[Option[Boolean]]] = unboxedPairBoolean._2

private val unboxedPairInteger = unboxedPair[java.lang.Integer, Int](_.intValue, java.lang.Integer.valueOf)
private val unboxedPairShort = unboxedPair[java.lang.Short, Short](Short2short, short2Short)
implicit val unliftedUnboxedShortArrayType: Meta[Array[Short]] = unboxedPairShort._1
implicit val liftedUnboxedShortArrayType: Meta[Array[Option[Short]]] = unboxedPairShort._2

private val unboxedPairInteger = unboxedPair[java.lang.Integer, Int](Integer2int, int2Integer)
implicit val unliftedUnboxedIntegerArrayType: Meta[Array[Int]] = unboxedPairInteger._1
implicit val liftedUnboxedIntegerArrayType: Meta[Array[Option[Int]]] = unboxedPairInteger._2

private val unboxedPairLong = unboxedPair[java.lang.Long, Long](_.longValue, java.lang.Long.valueOf)
private val unboxedPairLong = unboxedPair[java.lang.Long, Long](Long2long, long2Long)
implicit val unliftedUnboxedLongArrayType: Meta[Array[Long]] = unboxedPairLong._1
implicit val liftedUnboxedLongArrayType: Meta[Array[Option[Long]]] = unboxedPairLong._2

private val unboxedPairFloat = unboxedPair[java.lang.Float, Float](_.floatValue, java.lang.Float.valueOf)
private val unboxedPairFloat = unboxedPair[java.lang.Float, Float](Float2float, float2Float)
implicit val unliftedUnboxedFloatArrayType: Meta[Array[Float]] = unboxedPairFloat._1
implicit val liftedUnboxedFloatArrayType: Meta[Array[Option[Float]]] = unboxedPairFloat._2

private val unboxedPairDouble = unboxedPair[java.lang.Double, Double](_.doubleValue, java.lang.Double.valueOf)
private val unboxedPairDouble = unboxedPair[java.lang.Double, Double](Double2double, double2Double)
implicit val unliftedUnboxedDoubleArrayType: Meta[Array[Double]] = unboxedPairDouble._1
implicit val liftedUnboxedDoubleArrayType: Meta[Array[Option[Double]]] = unboxedPairDouble._2

Expand All @@ -135,17 +160,4 @@ object array {
)(
_.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.

}
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,7 @@ object PostgresCheckSuite extends PostgresDatabaseSpec {
}

private def errorWrite[A: Put](value: A, dbType: String) = for {
analysisResult <-
fr"SELECT $value::${Fragment.const(dbType)}".update.analysis.transact
analysisResult <- fr"SELECT $value::${Fragment.const(dbType)}".update.analysis.transact
} yield {
val errorClasses = analysisResult.parameterAlignmentErrors.map(_.getClass)
assertTrue(errorClasses == List(classOf[ParameterTypeError]))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,17 @@ object PostgresTypesSuite extends PostgresDatabaseSpec {
skip("json"),

// 8.15 Arrays
skip("bit[]", "Requires a cast"),
skip("smallint[]", "always comes back as Array[Int]"),
suiteGetPut[List[Boolean]]("bit[]", Gen.listOfBounded(0, 10)(Gen.boolean)),
suiteGetPut[List[Short]]("smallint[]", Gen.listOfBounded(0, 10)(Gen.short)),
suiteGetPut[List[Int]]("integer[]", Gen.listOfBounded(0, 10)(Gen.int)),
suiteGetPut[List[Long]]("bigint[]", Gen.listOfBounded(0, 10)(Gen.long)),
suiteGetPut[List[Float]]("real[]", Gen.listOfBounded(0, 10)(Gen.float)),
suiteGetPut[List[Double]]("double precision[]", Gen.listOfBounded(0, 10)(Gen.double)),
suiteGetPut[List[String]]("varchar[]", Gen.listOfBounded(0, 10)(genString)),
suiteGetPut[List[UUID]]("uuid[]", Gen.listOfBounded(0, 10)(Gen.uuid)),
suiteGetPut[List[java.sql.Date]]("date[]", Gen.listOfBounded(0, 10)(genSQLDateArray)),
suiteGetPut[List[java.time.LocalDate]]("date[]", Gen.listOfBounded(0, 10)(genLocalDateArray)),
suiteGetPut[List[java.sql.Timestamp]]("timestamp[]", Gen.listOfBounded(0, 10)(genSQLTimestampArray)),
suiteGetPut("numeric[]", Gen.const(List[JBigDecimal](BigDecimal("3.14").bigDecimal, BigDecimal("42.0").bigDecimal))),
suiteGetPut("numeric[]", Gen.const(List[BigDecimal](BigDecimal("3.14"), BigDecimal("42.0")))),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ object SQLGenerators {

val genSQLTime: Gen[Any, Time] = TimeGenerators.genLocalTime.map(Time.valueOf(_))
val genSQLDate: Gen[Any, Date] = TimeGenerators.genLocalDate.map(Date.valueOf(_))
val genSQLDateArray: Gen[Any, Date] = TimeGenerators.genLocalDateArray.map(Date.valueOf(_))
val genSQLTimestamp: Gen[Any, Timestamp] = TimeGenerators.genLocalDateTime.map(Timestamp.valueOf(_))
val genSQLTimestampArray: Gen[Any, Timestamp] = TimeGenerators.genLocalDateTimeArray.map(Timestamp.valueOf(_))

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ object TimeGenerators {
// 4713 BC to 5874897 AD
val genLocalDate: Gen[Any, LocalDate] = Gen.localDate(MinDate, MaxDate)

val genLocalDateArray: Gen[Any, LocalDate] = Gen.localDate(LocalDate.of(1, 1, 1), LocalDate.of(9999, 12, 31))

// 00:00:00.000000 to 23:59:59.999999
val genLocalTime: Gen[Any, LocalTime] = {
val min = micros(LocalTime.MIN.toNanoOfDay)
Expand All @@ -40,6 +42,11 @@ object TimeGenerators {
time <- genLocalTime
} yield LocalDateTime.of(date, time)

val genLocalDateTimeArray: Gen[Any, LocalDateTime] = for {
date <- genLocalDateArray
time <- genLocalTime
} yield LocalDateTime.of(date, time)

val genInstant: Gen[Any, Instant] = {
genLocalDateTime.map(_.toInstant(ZoneOffset.UTC))
}
Expand Down
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.11.4
sbt.version=1.11.7