Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,11 @@ jobs:

- name: Make target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: mkdir -p modules/skunk/js/target modules/sql-core/.js/target modules/circe/.jvm/target modules/generic/.jvm/target modules/doobie-pg/target unidocs/target modules/core/.native/target modules/skunk/jvm/target modules/core/.js/target modules/doobie-core/target modules/circe/.js/target modules/skunk/native/target modules/generic/.js/target modules/doobie-oracle/target modules/sql-core/.jvm/target modules/core/.jvm/target modules/sql-pg/native/target modules/doobie-mssql/target modules/sql-pg/js/target modules/circe/.native/target modules/generic/.native/target modules/sql-pg/jvm/target modules/sql-core/.native/target project/target
run: mkdir -p modules/skunk/js/target modules/sql-core/.js/target modules/circe/.jvm/target modules/generic/.jvm/target modules/doobie-pg/target unidocs/target modules/core/.native/target modules/skunk/jvm/target modules/core/.js/target modules/doobie-core/target modules/circe/.js/target modules/doobie-h2/target modules/skunk/native/target modules/generic/.js/target modules/doobie-oracle/target modules/sql-core/.jvm/target modules/core/.jvm/target modules/sql-pg/native/target modules/doobie-mssql/target modules/sql-pg/js/target modules/doobie-sqlite/target modules/circe/.native/target modules/generic/.native/target modules/sql-pg/jvm/target modules/sql-core/.native/target project/target

- name: Compress target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
run: tar cf targets.tar modules/skunk/js/target modules/sql-core/.js/target modules/circe/.jvm/target modules/generic/.jvm/target modules/doobie-pg/target unidocs/target modules/core/.native/target modules/skunk/jvm/target modules/core/.js/target modules/doobie-core/target modules/circe/.js/target modules/skunk/native/target modules/generic/.js/target modules/doobie-oracle/target modules/sql-core/.jvm/target modules/core/.jvm/target modules/sql-pg/native/target modules/doobie-mssql/target modules/sql-pg/js/target modules/circe/.native/target modules/generic/.native/target modules/sql-pg/jvm/target modules/sql-core/.native/target project/target
run: tar cf targets.tar modules/skunk/js/target modules/sql-core/.js/target modules/circe/.jvm/target modules/generic/.jvm/target modules/doobie-pg/target unidocs/target modules/core/.native/target modules/skunk/jvm/target modules/core/.js/target modules/doobie-core/target modules/circe/.js/target modules/doobie-h2/target modules/skunk/native/target modules/generic/.js/target modules/doobie-oracle/target modules/sql-core/.jvm/target modules/core/.jvm/target modules/sql-pg/native/target modules/doobie-mssql/target modules/sql-pg/js/target modules/doobie-sqlite/target modules/circe/.native/target modules/generic/.native/target modules/sql-pg/jvm/target modules/sql-core/.native/target project/target

- name: Upload target directories
if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main')
Expand Down
73 changes: 72 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ val munitScalaCheckVersion = "1.3.0"
val oracleDriverVersion = "23.26.2.0.0"
val postgresVersion = "42.7.13"
val skunkVersion = "1.0.0"
val sqliteDriverVersion = "3.53.2.0"
val h2DriverVersion = "2.4.240"
val shapeless2Version = "2.3.13"
val shapeless3Version = "3.6.0"
val sourcePosVersion = "1.2.0"
Expand All @@ -36,7 +38,7 @@ ThisBuild / scalaVersion := Scala2
ThisBuild / crossScalaVersions := Seq(Scala2, Scala3)
ThisBuild / tlJdkRelease := Some(11)

ThisBuild / tlBaseVersion := "0.29"
ThisBuild / tlBaseVersion := "0.30"
ThisBuild / startYear := Some(2019)
ThisBuild / licenses := Seq(License.Apache2)
ThisBuild / developers := List(
Expand Down Expand Up @@ -171,6 +173,13 @@ lazy val nativeSettings = Seq(
)
)

// Pin the forked test JVM to UTC: mssql-jdbc binds a zone-naive java.sql.Timestamp using the
// ambient JVM zone, so MSSQL datetime tests fail off-UTC unless pinned. Only meaningful where
// Test / fork := true.
lazy val utcTestSettings = Seq(
Test / javaOptions += "-Duser.timezone=UTC"
)

lazy val modules: List[CompositeProject] = List(
core,
circe,
Expand All @@ -180,6 +189,8 @@ lazy val modules: List[CompositeProject] = List(
doobiepg,
doobieoracle,
doobiemssql,
doobiesqlite,
doobieh2,
skunk,
generic,
docs,
Expand Down Expand Up @@ -272,6 +283,7 @@ lazy val doobiecore = project
.disablePlugins(RevolverPlugin)
.dependsOn(sqlcore.jvm % "test->test;compile->compile", circe.jvm)
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-core",
Test / fork := true,
Expand All @@ -291,6 +303,7 @@ lazy val doobiepg = project
doobiecore % "test->test;compile->compile",
sqlpg.jvm % "test->test;compile->compile")
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-pg",
Test / fork := true,
Expand All @@ -310,6 +323,7 @@ lazy val doobieoracle = project
.disablePlugins(RevolverPlugin)
.dependsOn(doobiecore % "test->test;compile->compile")
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-oracle",
Test / fork := true,
Expand All @@ -327,6 +341,7 @@ lazy val doobiemssql = project
.disablePlugins(RevolverPlugin)
.dependsOn(doobiecore % "test->test;compile->compile")
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-mssql",
Test / fork := true,
Expand All @@ -338,6 +353,59 @@ lazy val doobiemssql = project
)
)

lazy val doobiesqlite = project
.in(file("modules/doobie-sqlite"))
.enablePlugins(AutomateHeaderPlugin)
.disablePlugins(RevolverPlugin)
.dependsOn(doobiecore % "test->test;compile->compile")
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-sqlite",
Test / fork := true,
Test / parallelExecution := false,
// SQLite has no docker service: unlike Oracle/MSSQL, whose containers auto-run the seed SQL
// mounted from testdata/<db>/, the test harness loads and executes testdata/sqlite/*.sql
// itself against a fresh temp database file per suite. Pass the directory as a system property
// (fork'd tests don't share the build's working directory) rather than relying on a relative
// path guess.
Test / javaOptions += s"-Dgrackle.sqlite.testdata=${(ThisBuild / baseDirectory).value / "testdata" / "sqlite"}",
// sqlite-jdbc's native cleanup on Connection#close touches JNI from what recent JDKs treat as
// a restricted context; without this the forked test JVM logs "restricted method" warnings and
// native handle teardown can throw spuriously. The flag only exists on JDK 17+ (JEP 412) -
// older JVMs, such as CI's temurin@11, refuse to start when given it (the forked JVM inherits
// the JDK sbt runs on), so it has to be supplied conditionally.
Test / javaOptions ++= {
if (sys.props("java.specification.version").toDouble >= 17)
Seq("--enable-native-access=ALL-UNNAMED")
else Nil
},
libraryDependencies ++= Seq(
"org.xerial" % "sqlite-jdbc" % sqliteDriverVersion
)
)

lazy val doobieh2 = project
.in(file("modules/doobie-h2"))
.enablePlugins(AutomateHeaderPlugin)
.disablePlugins(RevolverPlugin)
.dependsOn(doobiecore % "test->test;compile->compile")
.settings(commonSettings)
.settings(utcTestSettings)
.settings(
name := "grackle-doobie-h2",
Test / fork := true,
Test / parallelExecution := false,
// H2 has no docker service: the test harness seeds a fresh in-memory database per suite
// from testdata/h2/*.sql. Pass the directory as a system property (fork'd tests don't share
// the build's working directory).
Test / javaOptions += s"-Dgrackle.h2.testdata=${(ThisBuild / baseDirectory).value / "testdata" / "h2"}",
libraryDependencies ++= Seq(
"org.typelevel" %% "doobie-h2" % doobieVersion,
"com.h2database" % "h2" % h2DriverVersion
)
)

lazy val skunk = crossProject(JVMPlatform, JSPlatform, NativePlatform)
.crossType(CrossType.Full)
.in(file("modules/skunk"))
Expand All @@ -354,6 +422,7 @@ lazy val skunk = crossProject(JVMPlatform, JSPlatform, NativePlatform)
"org.typelevel" %% "log4cats-core" % log4catsVersion
)
)
.jvmSettings(utcTestSettings)
.jvmSettings(
Test / fork := true,
Test / testOptions += Tests.Setup(_ =>
Expand Down Expand Up @@ -486,6 +555,8 @@ lazy val unidocs = project
doobiepg,
doobieoracle,
doobiemssql,
doobiesqlite,
doobieh2,
skunk.jvm,
generic.jvm
)
Expand Down
3 changes: 1 addition & 2 deletions modules/doobie-core/src/test/scala/DoobieDatabaseSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ package grackle.doobie.test
import java.time.Duration

import io.circe.{Decoder => CDecoder, Encoder => CEncoder}
import munit.CatsEffectSuite
import org.typelevel.doobie.Meta

import grackle.doobie.DoobieMappingLike
import grackle.sql.test._

trait DoobieDatabaseSuite extends CatsEffectSuite {
trait DoobieDatabaseSuite extends SqlDatabaseSuite {
trait DoobieTestMapping[F[_]] extends DoobieMappingLike[F] with SqlTestMapping[F] {

type TestCodec[T] = (Meta[T], Boolean)
Expand Down
123 changes: 123 additions & 0 deletions modules/doobie-h2/src/main/scala/DoobieH2Mapping.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright (c) 2016-2025 Association of Universities for Research in Astronomy, Inc. (AURA)
// Copyright (c) 2016-2025 Grackle Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package grackle.doobie.h2

import cats.effect.Sync
import cats.syntax.all._
import org.typelevel.doobie.Transactor

import grackle.Mapping
import grackle.Query.OrderSelection
import grackle.doobie._
import grackle.sql._

abstract class DoobieH2Mapping[F[_]](
val transactor: Transactor[F],
val monitor: DoobieMonitor[F]
)(
implicit val M: Sync[F]
) extends Mapping[F]
with DoobieH2MappingLike[F]

/**
* H2 (REGULAR mode) is close to Postgres for the constructs the shared query builder needs -
* ILIKE, DISTINCT ON, NULLS FIRST/LAST and parenthesized union branches are all native - with
* three exceptions: offset/limit render as standard OFFSET .. ROWS / FETCH NEXT .. ROWS ONLY
* (as Oracle); there is no LATERAL join, which `mkLateral` answers with `NotLateral` exactly as
* the SQLite backend does (see `supportsLateralJoin`'s doc comment for the consequences); and
* although modern H2 supports `NULLS FIRST`/`NULLS LAST` natively, unlike Postgres/Oracle H2's
* default places NULLs low (first in ASC, last in DESC), so `orderToFragment` emits the
* explicit clause on the mirror-image cases relative to the pg dialect - the same polarity
* correction the MSSQL dialect makes.
*/
trait DoobieH2MappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingLike[F] {
import SqlQuery.SqlSelect
import TableExpr.Laterality

// H2 has no per-expression COLLATE, and its default ordering is already code-point order -
// the very thing the other dialects' COLLATE "C"/BINARY opt into - so nothing needs to be
// emitted on the rare collated-rendering paths either.
def collateToFragment: Fragment = Fragments.empty

def aliasDefToFragment(alias: String): Fragment =
Fragments.const(s" AS $alias")

// Standard SQL OFFSET/FETCH, as Oracle renders it. In H2 each clause is independently legal,
// in the offset-then-limit order the shared builder renders, with or without ORDER BY - so no
// offset/limit normalization is needed (normalizeOffsetLimit below is the identity).
def offsetToFragment(offset: Fragment): Fragment =
Fragments.const(" OFFSET ") |+| offset |+| Fragments.const(" ROWS")

def limitToFragment(limit: Fragment): Fragment =
Fragments.const(" FETCH NEXT ") |+| limit |+| Fragments.const(" ROWS ONLY")

// H2 supports ILIKE natively in REGULAR mode, same as Postgres.
def likeToFragment(expr: Fragment, pattern: String, caseInsensitive: Boolean): Fragment = {
val op = if (caseInsensitive) "ILIKE" else "LIKE"
expr |+| Fragments.const(s" $op ") |+| Fragments.bind(stringEncoder, pattern)
}

// H2's CAST accepts any type name its driver reports.
def ascribedNullToFragment(codec: Codec): Fragment =
Fragments.sqlTypeName(codec) match {
case Some(name) => Fragments.const(s"CAST(NULL AS $name)")
case None => Fragments.const("NULL")
}

def collateSelected: Boolean = false

// H2 supports DISTINCT ON with Postgres semantics (first row per group under ORDER BY).
def distinctOnToFragment(dcols: List[Fragment]): Fragment =
Fragments.const("DISTINCT ON ") |+| Fragments.parentheses(
dcols.intercalate(Fragments.const(", ")))

def distinctOrderColumn(
owner: ColumnOwner,
col: SqlColumn,
predCols: List[SqlColumn],
orders: List[OrderSelection[_]]): SqlColumn = col

// A parenthesized compound-select branch may carry its own ORDER BY/OFFSET/FETCH inline (an
// unparenthesized one may not - the parentheses supplied by unionBranchToFragment are
// load-bearing), so no derived-table wrapping is needed.
def encapsulateUnionBranch(s: SqlSelect): SqlSelect = s
def unionBranchToFragment(branch: Fragment): Fragment = Fragments.parentheses(branch)

// H2 has no LATERAL (or APPLY) mechanism, so NotLateral (plain subquery, no keyword) is the
// only possible rendering; SqlMappingLike derives supportsLateralJoin = false from it - see
// that member's doc comment.
def mkLateral(inner: Boolean): Laterality = Laterality.NotLateral

def normalizeOffsetLimit(query: SqlQuery): SqlQuery = query
def defaultOffsetForLimit(limit: Option[Int]): Option[Int] = None

def orderToFragment(col: Fragment, ascending: Boolean, nullsLast: Boolean): Fragment = {
val dir = if (ascending) Fragments.empty else Fragments.const(" DESC")
val nulls =
if (nullsLast && ascending)
Fragments.const(" NULLS LAST ")
else if (!nullsLast && !ascending)
Fragments.const(" NULLS FIRST ")
else
Fragments.empty

col |+| dir |+| nulls
}

// H2 sorts NULL below any non-NULL value by default (NULLs first in ASC), the same convention
// as MSSQL and SQLite.
def nullsHigh: Boolean = false
}
Loading
Loading