-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
49f72ac
commit dde325a
Showing
30 changed files
with
6,451 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
111 changes: 111 additions & 0 deletions
111
modules/doobie-mssql/src/main/scala/DoobieMSSqlMapping.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// Copyright (c) 2016-2023 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.mssql | ||
|
||
import cats.effect.Sync | ||
import cats.syntax.all._ | ||
import _root_.doobie.Transactor | ||
|
||
import grackle.Mapping | ||
import grackle.Query.OrderSelection | ||
import grackle.doobie._ | ||
import grackle.sql._ | ||
|
||
abstract class DoobieMSSqlMapping[F[_]]( | ||
val transactor: Transactor[F], | ||
val monitor: DoobieMonitor[F], | ||
)( | ||
implicit val M: Sync[F] | ||
) extends Mapping[F] with DoobieMSSqlMappingLike[F] | ||
|
||
trait DoobieMSSqlMappingLike[F[_]] extends DoobieMappingLike[F] with SqlMappingLike[F] { | ||
import SqlQuery.SqlSelect | ||
import TableExpr.Laterality | ||
|
||
def collateToFragment: Fragment = | ||
Fragments.const(" COLLATE DATABASE_DEFAULT") | ||
|
||
def aliasDefToFragment(alias: String): Fragment = | ||
Fragments.const(s" $alias") | ||
|
||
def offsetToFragment(offset: Fragment): Fragment = | ||
Fragments.const(" OFFSET ") |+| offset |+| Fragments.const(" ROWS") | ||
|
||
def limitToFragment(limit: Fragment): Fragment = | ||
Fragments.const(" FETCH FIRST ") |+| limit |+| Fragments.const(" ROWS ONLY") | ||
|
||
def likeToFragment(expr: Fragment, pattern: String, caseInsensitive: Boolean): Fragment = { | ||
val casedExpr = if(caseInsensitive) Fragments.const("UPPER(") |+| expr |+| Fragments.const(s")") else expr | ||
val casedPattern = if(caseInsensitive) pattern.toUpperCase else pattern | ||
casedExpr |+| Fragments.const(s" LIKE ") |+| Fragments.bind(stringEncoder, casedPattern) | ||
} | ||
|
||
def ascribedNullToFragment(codec: Codec): Fragment = | ||
Fragments.sqlTypeName(codec) match { | ||
case Some(name) if !name.startsWith("_") => | ||
val convName = | ||
name match { | ||
case "VARCHAR" => "CHAR" | ||
case "NVARCHAR" => "NCHAR" | ||
case "INTEGER" => "INTEGER" | ||
case "BIGINT" => "BIGINT" | ||
case "BOOLEAN" => "BIT" | ||
case "TIMESTAMP" => "DATETIMEOFFSET" // TODO: Probably shouldn't be TIMESTAMP on the LHS | ||
case other => other | ||
} | ||
Fragments.const(s"CAST(NULL AS $convName)") | ||
case _ => Fragments.const("NULL") | ||
} | ||
|
||
def collateSelected: Boolean = false | ||
|
||
def distinctOnToFragment(dcols: List[Fragment]): Fragment = | ||
Fragments.const("DISTINCT ") | ||
|
||
def distinctOrderColumn(owner: ColumnOwner, col: SqlColumn, predCols: List[SqlColumn], orders: List[OrderSelection[_]]): SqlColumn = | ||
SqlColumn.FirstValueColumn(owner, col, predCols, orders) | ||
|
||
def encapsulateUnionBranch(s: SqlSelect): SqlSelect = | ||
if(s.orders.isEmpty) s | ||
else s.toSubquery(s.table.name+"_encaps", Laterality.NotLateral) | ||
|
||
def mkLateral(inner: Boolean): Laterality = | ||
Laterality.Apply(inner) | ||
|
||
def defaultOffsetForSubquery(subquery: SqlQuery): SqlQuery = | ||
subquery match { | ||
case s: SqlSelect if s.orders.nonEmpty && s.offset.isEmpty => s.copy(offset = 0.some) | ||
case _ => subquery | ||
} | ||
|
||
def defaultOffsetForLimit(limit: Option[Int]): Option[Int] = | ||
limit.as(0) | ||
|
||
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(" CASE WHEN ") |+| col |+| Fragments.const(" IS NULL THEN 1 ELSE 0 END ASC, ") | ||
else if(!nullsLast && !ascending) | ||
Fragments.const(" CASE WHEN ") |+| col |+| Fragments.const(" IS NULL THEN 0 ELSE 1 END DESC, ") | ||
else | ||
Fragments.empty | ||
|
||
nulls |+| col |+| dir | ||
} | ||
|
||
def nullsHigh: Boolean = false | ||
} |
29 changes: 29 additions & 0 deletions
29
modules/doobie-mssql/src/test/resources/scripts/entrypoint.sh
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
#!/bin/bash | ||
|
||
# Start SQL Server | ||
/opt/mssql/bin/sqlservr & | ||
|
||
# Wait for SQL Server to start (max 90 seconds) | ||
for i in {1..90}; do | ||
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Test_123_Test -No -Q 'SELECT 1' &> /dev/null | ||
if [ $? -eq 0 ]; then | ||
echo "SQL Server is up" | ||
break | ||
fi | ||
echo "Waiting for SQL Server to start..." | ||
sleep 1 | ||
done | ||
|
||
# Initialize the database | ||
if [ ! -f /tmp/healthy ]; then | ||
echo "Intializing the database ..." | ||
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P Test_123_Test -No \ | ||
-i <(cat /container-entrypoint-initdb.d/init.sql /grackle-initdb.d/*.sql) | ||
|
||
/bin/touch /tmp/healthy | ||
|
||
echo "Database initialized" | ||
fi | ||
|
||
# Keep the container running | ||
tail -f /dev/null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
USE master; | ||
GO | ||
|
||
CREATE DATABASE test | ||
COLLATE Latin1_General_100_CI_AS_SC_UTF8; | ||
GO | ||
|
||
USE test; | ||
GO |
105 changes: 105 additions & 0 deletions
105
modules/doobie-mssql/src/test/scala/DoobieMSSqlDatabaseSuite.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) | ||
// Copyright (c) 2016-2023 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.mssql | ||
package test | ||
|
||
import java.sql.{Time, Timestamp} | ||
import java.time.{LocalDate, LocalTime, OffsetDateTime, ZoneId} | ||
import java.util.UUID | ||
import scala.util.Try | ||
|
||
import cats.effect.{Resource, Sync, IO} | ||
import cats.syntax.all._ | ||
import doobie.{Meta, Transactor} | ||
import doobie.enumerated.JdbcType | ||
import doobie.util.meta.MetaConstructors.Basic | ||
import io.circe.{Decoder => CDecoder, Encoder => CEncoder, Json} | ||
import io.circe.syntax._ | ||
import io.circe.parser.parse | ||
import munit.catseffect._ | ||
|
||
import grackle.doobie.DoobieMonitor | ||
import grackle.doobie.test.DoobieDatabaseSuite | ||
|
||
import grackle.sql.test._ | ||
|
||
trait DoobieMSSqlDatabaseSuite extends DoobieDatabaseSuite { | ||
abstract class DoobieMSSqlTestMapping[F[_]: Sync](transactor: Transactor[F], monitor: DoobieMonitor[F] = DoobieMonitor.noopMonitor[IO]) | ||
extends DoobieMSSqlMapping[F](transactor, monitor) with DoobieTestMapping[F] with SqlTestMapping[F] { | ||
def mkTestCodec[T](meta: Meta[T]): TestCodec[T] = (meta, false) | ||
|
||
val uuid: TestCodec[UUID] = | ||
mkTestCodec(Meta[String].tiemap(s => Try(UUID.fromString(s)).toEither.leftMap(_.getMessage))(_.toString)) | ||
|
||
val localTime: TestCodec[LocalTime] = { | ||
mkTestCodec(Meta[Time].timap(t => LocalTime.ofNanoOfDay(t.toLocalTime.toNanoOfDay))(lt => Time.valueOf(lt))) | ||
} | ||
|
||
val localDate: TestCodec[LocalDate] = | ||
(Basic.oneObject(JdbcType.Date, None, classOf[LocalDate]), false) | ||
|
||
// Forget precise time zone for compatibility with Postgres. Nb. this is specific to this test suite. | ||
val offsetDateTime: TestCodec[OffsetDateTime] = | ||
mkTestCodec(Meta[Timestamp].timap(t => OffsetDateTime.ofInstant(t.toInstant, ZoneId.of("UTC")))(o => Timestamp.from(o.toInstant))) | ||
|
||
val nvarchar: TestCodec[String] = mkTestCodec(Meta[String]) | ||
|
||
val jsonb: TestCodec[Json] = | ||
mkTestCodec(Meta[String].tiemap(s => parse(s).leftMap(_.getMessage))(_.noSpaces)) | ||
|
||
override def list[T: CDecoder : CEncoder](c: TestCodec[T]): TestCodec[List[T]] = { | ||
def put(ts: List[T]): String = ts.asJson.noSpaces | ||
def get(s: String): Either[String, List[T]] = parse(s).map(_.as[List[T]].toOption.get).leftMap(_.getMessage) | ||
|
||
mkTestCodec(Meta[String].tiemap(get)(put)) | ||
} | ||
} | ||
|
||
case class MSSqlConnectionInfo(host: String, port: Int) { | ||
val driverClassName = "com.microsoft.sqlserver.jdbc.SQLServerDriver" | ||
val databaseName = "test" | ||
val username = "sa" | ||
val password = "Test_123_Test" | ||
val jdbcUrl = s"jdbc:sqlserver://$host:$port;databaseName=$databaseName;user=$username;password=$password;trustServerCertificate=true;sendTimeAsDatetime=false;" | ||
} | ||
|
||
object MSSqlConnectionInfo { | ||
val DefaultPort = 1433 | ||
} | ||
|
||
val msSqlConnectionInfo: MSSqlConnectionInfo = | ||
MSSqlConnectionInfo("localhost", MSSqlConnectionInfo.DefaultPort) | ||
|
||
def transactorResource: Resource[IO, Transactor[IO]] = { | ||
val connInfo = msSqlConnectionInfo | ||
import connInfo._ | ||
|
||
val props = new java.util.Properties() | ||
Resource.pure( | ||
Transactor.fromDriverManager[IO]( | ||
driverClassName, | ||
jdbcUrl, | ||
props, | ||
None | ||
) | ||
) | ||
} | ||
|
||
val transactorFixture: IOFixture[Transactor[IO]] = ResourceSuiteLocalFixture("mssqlpg", transactorResource) | ||
override def munitFixtures: Seq[IOFixture[_]] = Seq(transactorFixture) | ||
|
||
def transactor: Transactor[IO] = transactorFixture() | ||
} |
Oops, something went wrong.