diff options
Diffstat (limited to 'core-database/src/main/scala/xyz')
6 files changed, 384 insertions, 0 deletions
diff --git a/core-database/src/main/scala/xyz/driver/core/database/Converters.scala b/core-database/src/main/scala/xyz/driver/core/database/Converters.scala new file mode 100644 index 0000000..b0054ad --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/Converters.scala @@ -0,0 +1,25 @@ +package xyz.driver.core.database + +import scala.reflect.ClassTag + +/** + * Helper methods for converting between table rows and Scala objects + */ +trait Converters { + def fromStringOrThrow[ADT](entityStr: String, mapper: (String => Option[ADT]), entityName: String): ADT = + mapper(entityStr).getOrElse(throw DatabaseException(s"Invalid $entityName in database: $entityStr")) + + def expectValid[ADT](mapper: String => Option[ADT], query: String)(implicit ct: ClassTag[ADT]): ADT = + fromStringOrThrow[ADT](query, mapper, ct.toString()) + + def expectExistsAndValid[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( + implicit ct: ClassTag[ADT]): ADT = { + expectValid[ADT](mapper, query.getOrElse(throw DatabaseException(contextMsg))) + } + + def expectValidOrEmpty[ADT](mapper: String => Option[ADT], query: Option[String], contextMsg: String = "")( + implicit ct: ClassTag[ADT]): Option[ADT] = { + query.map(expectValid[ADT](mapper, _)) + } +} +final case class DatabaseException(message: String = "Database access error") extends RuntimeException(message) diff --git a/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala b/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala new file mode 100644 index 0000000..e2efd32 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/PatchedHsqldbProfile.scala @@ -0,0 +1,16 @@ +package xyz.driver.core.database + +import slick.jdbc.{HsqldbProfile, JdbcType} +import slick.ast.FieldSymbol +import slick.relational.RelationalProfile + +trait PatchedHsqldbProfile extends HsqldbProfile { + override def defaultSqlTypeName(tmd: JdbcType[_], sym: Option[FieldSymbol]): String = tmd.sqlType match { + case java.sql.Types.VARCHAR => + val size = sym.flatMap(_.findColumnOption[RelationalProfile.ColumnOption.Length]) + size.fold("LONGVARCHAR")(l => if (l.varying) s"VARCHAR(${l.length})" else s"CHAR(${l.length})") + case _ => super.defaultSqlTypeName(tmd, sym) + } +} + +object PatchedHsqldbProfile extends PatchedHsqldbProfile diff --git a/core-database/src/main/scala/xyz/driver/core/database/Repository.scala b/core-database/src/main/scala/xyz/driver/core/database/Repository.scala new file mode 100644 index 0000000..5d7f787 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/Repository.scala @@ -0,0 +1,74 @@ +package xyz.driver.core.database + +import scalaz.std.scalaFuture._ +import scalaz.{ListT, Monad, OptionT} +import slick.lifted.{AbstractTable, CanBeQueryCondition, RunnableCompiled} +import slick.{lifted => sl} + +import scala.concurrent.{ExecutionContext, Future} +import scala.language.higherKinds + +trait Repository { + type T[D] + implicit def monadT: Monad[T] + + def execute[D](operations: T[D]): Future[D] + def noAction[V](v: V): T[V] + def customAction[R](action: => Future[R]): T[R] + + def customAction[R](action: => OptionT[Future, R]): OptionT[T, R] = + OptionT[T, R](customAction(action.run)) +} + +class FutureRepository(executionContext: ExecutionContext) extends Repository { + implicit val exec: ExecutionContext = executionContext + override type T[D] = Future[D] + implicit val monadT: Monad[Future] = implicitly[Monad[Future]] + + def execute[D](operations: T[D]): Future[D] = operations + def noAction[V](v: V): T[V] = Future.successful(v) + def customAction[R](action: => Future[R]): T[R] = action +} + +class SlickRepository(database: Database, executionContext: ExecutionContext) extends Repository { + import database.profile.api._ + implicit val exec: ExecutionContext = executionContext + + override type T[D] = slick.dbio.DBIO[D] + + implicit protected class QueryOps[+E, U](query: Query[E, U, Seq]) { + def resultT: ListT[T, U] = ListT[T, U](query.result.map(_.toList)) + + def maybeFilter[V, R: CanBeQueryCondition](data: Option[V])(f: V => E => R): sl.Query[E, U, Seq] = + data.map(v => query.withFilter(f(v))).getOrElse(query) + } + + implicit protected class CompiledQueryOps[U](compiledQuery: RunnableCompiled[_, Seq[U]]) { + def resultT: ListT[T, U] = ListT.listT[T](compiledQuery.result.map(_.toList)) + } + + private val dbioMonad = new Monad[T] { + override def point[A](a: => A): T[A] = DBIO.successful(a) + + override def bind[A, B](fa: T[A])(f: A => T[B]): T[B] = fa.flatMap(f) + } + + override implicit def monadT: Monad[T] = dbioMonad + + override def execute[D](readOperations: T[D]): Future[D] = { + database.database.run(readOperations.transactionally) + } + + override def noAction[V](v: V): T[V] = DBIO.successful(v) + + override def customAction[R](action: => Future[R]): T[R] = DBIO.from(action) + + def affectsRows(updatesCount: Int): Option[Unit] = { + if (updatesCount > 0) Some(()) else None + } + + def insertReturning[AT <: AbstractTable[_], V](table: TableQuery[AT])( + row: AT#TableElementType): slick.dbio.DBIO[AT#TableElementType] = { + table.returning(table) += row + } +} diff --git a/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala b/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala new file mode 100644 index 0000000..8293371 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/SlickGetResultSupport.scala @@ -0,0 +1,30 @@ +package xyz.driver.core.database + +import slick.jdbc.GetResult +import xyz.driver.core.date.Date +import xyz.driver.core.time.Time +import xyz.driver.core.{Id, Name} + +trait SlickGetResultSupport { + implicit def GetId[U]: GetResult[Id[U]] = + GetResult(r => Id[U](r.nextString())) + implicit def GetIdOption[U]: GetResult[Option[Id[U]]] = + GetResult(_.nextStringOption().map(Id.apply[U])) + + implicit def GetName[U]: GetResult[Name[U]] = + GetResult(r => Name[U](r.nextString())) + implicit def GetNameOption[U]: GetResult[Option[Name[U]]] = + GetResult(_.nextStringOption().map(Name.apply[U])) + + implicit val GetTime: GetResult[Time] = + GetResult(r => Time(r.nextTimestamp.getTime)) + implicit val GetTimeOption: GetResult[Option[Time]] = + GetResult(_.nextTimestampOption().map(t => Time(t.getTime))) + + implicit val GetDate: GetResult[Date] = + GetResult(r => sqlDateToDate(r.nextDate())) + implicit val GetDateOption: GetResult[Option[Date]] = + GetResult(_.nextDateOption().map(sqlDateToDate)) +} + +object SlickGetResultSupport extends SlickGetResultSupport diff --git a/core-database/src/main/scala/xyz/driver/core/database/database.scala b/core-database/src/main/scala/xyz/driver/core/database/database.scala new file mode 100644 index 0000000..bd20b54 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/database.scala @@ -0,0 +1,178 @@ +package xyz.driver.core + +import slick.basic.DatabaseConfig +import slick.jdbc.JdbcProfile +import xyz.driver.core.date.Date +import xyz.driver.core.time.Time + +import scala.concurrent.Future +import com.typesafe.config.Config + +package database { + + import java.sql.SQLDataException + import java.time.{Instant, LocalDate} + + import eu.timepit.refined.api.{Refined, Validate} + import eu.timepit.refined.refineV + + trait Database { + val profile: JdbcProfile + val database: JdbcProfile#Backend#Database + } + + object Database { + def fromConfig(config: Config, databaseName: String): Database = { + val dbConfig: DatabaseConfig[JdbcProfile] = DatabaseConfig.forConfig(databaseName, config) + + new Database { + val profile: JdbcProfile = dbConfig.profile + val database: JdbcProfile#Backend#Database = dbConfig.db + } + } + + def fromConfig(databaseName: String): Database = { + fromConfig(com.typesafe.config.ConfigFactory.load(), databaseName) + } + } + + trait ColumnTypes { + val profile: JdbcProfile + } + + trait NameColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] + } + + object NameColumnTypes { + trait StringName extends NameColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Name.columnType`[T]: BaseColumnType[Name[T]] = + MappedColumnType.base[Name[T], String](_.value, Name[T]) + } + } + + trait DateColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] + implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] + } + + object DateColumnTypes { + trait SqlDate extends DateColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Date.columnType`: BaseColumnType[Date] = + MappedColumnType.base[Date, java.sql.Date](dateToSqlDate, sqlDateToDate) + + override implicit def `java.time.LocalDate.columnType`: BaseColumnType[LocalDate] = + MappedColumnType.base[LocalDate, java.sql.Date](java.sql.Date.valueOf, _.toLocalDate) + } + } + + trait RefinedColumnTypes[T, Predicate] extends ColumnTypes { + import profile.api._ + implicit def `eu.timepit.refined.api.Refined`( + implicit columnType: BaseColumnType[T], + validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] + } + + object RefinedColumnTypes { + trait RefinedValue[T, Predicate] extends RefinedColumnTypes[T, Predicate] { + import profile.api._ + override implicit def `eu.timepit.refined.api.Refined`( + implicit columnType: BaseColumnType[T], + validate: Validate[T, Predicate]): BaseColumnType[T Refined Predicate] = + MappedColumnType.base[T Refined Predicate, T]( + _.value, { dbValue => + refineV[Predicate](dbValue) match { + case Left(refinementError) => + throw new SQLDataException( + s"Value in the database doesn't match the refinement constraints: $refinementError") + case Right(refinedValue) => + refinedValue + } + } + ) + } + } + + trait IdColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.Id.columnType`[T]: BaseColumnType[Id[T]] + } + + object IdColumnTypes { + trait UUID extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType + .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + } + trait SerialId extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + } + trait NaturalId extends IdColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.Id.columnType`[T] = + MappedColumnType.base[Id[T], String](_.value, Id[T]) + } + } + + trait TimestampColumnTypes extends ColumnTypes { + import profile.api._ + implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] + implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] + } + + object TimestampColumnTypes { + trait SqlTimestamp extends TimestampColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = + MappedColumnType.base[Time, java.sql.Timestamp]( + time => new java.sql.Timestamp(time.millis), + timestamp => Time(timestamp.getTime)) + + override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = + MappedColumnType.base[Instant, java.sql.Timestamp](java.sql.Timestamp.from, _.toInstant) + } + + trait PrimitiveTimestamp extends TimestampColumnTypes { + import profile.api._ + + override implicit def `xyz.driver.core.time.Time.columnType`: BaseColumnType[Time] = + MappedColumnType.base[Time, Long](_.millis, Time.apply) + + override implicit def `java.time.Instant.columnType`: BaseColumnType[Instant] = + MappedColumnType.base[Instant, Long](_.toEpochMilli, Instant.ofEpochMilli) + } + } + + trait KeyMappers extends ColumnTypes { + import profile.api._ + + def uuidKeyMapper[T] = + MappedColumnType + .base[Id[T], java.util.UUID](id => java.util.UUID.fromString(id.value), uuid => Id[T](uuid.toString)) + def serialKeyMapper[T] = MappedColumnType.base[Id[T], Long](_.value.toLong, serialId => Id[T](serialId.toString)) + def naturalKeyMapper[T] = MappedColumnType.base[Id[T], String](_.value, Id[T]) + } + + trait DatabaseObject extends ColumnTypes { + def createTables(): Future[Unit] + def disconnect(): Unit + } + + abstract class DatabaseObjectAdapter extends DatabaseObject { + def createTables(): Future[Unit] = Future.successful(()) + def disconnect(): Unit = {} + } +} diff --git a/core-database/src/main/scala/xyz/driver/core/database/package.scala b/core-database/src/main/scala/xyz/driver/core/database/package.scala new file mode 100644 index 0000000..aee14c6 --- /dev/null +++ b/core-database/src/main/scala/xyz/driver/core/database/package.scala @@ -0,0 +1,61 @@ +package xyz.driver.core + +import java.sql.{Date => SqlDate} +import java.util.Calendar + +import date.{Date, Month} +import slick.dbio._ +import slick.jdbc.JdbcProfile +import slick.relational.RelationalProfile + +package object database { + + type Schema = { + def create: DBIOAction[Unit, NoStream, Effect.Schema] + def drop: DBIOAction[Unit, NoStream, Effect.Schema] + } + + @deprecated( + "sbt-slick-codegen 0.11.0+ no longer needs to generate these methods. Please use the new `CodegenTables` trait when upgrading.", + "driver-core 1.8.12") + type GeneratedTables = { + // structure of Slick data model traits generated by sbt-slick-codegen + val profile: JdbcProfile + def schema: profile.SchemaDescription + + def createNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] + def dropNamespaceSchema: StreamingDBIO[Vector[Unit], Unit] + } + + /** A structural type for schema traits generated by sbt-slick-codegen. + * This will compile with codegen versions before 0.11.0, but note + * that methods in [[GeneratedTables]] are no longer generated. + */ + type CodegenTables[Profile <: RelationalProfile] = { + val profile: Profile + def schema: profile.SchemaDescription + } + + private[database] def sqlDateToDate(sqlDate: SqlDate): Date = { + // NOTE: SQL date does not have a time component, so this date + // should only be interpreted in the running JVMs timezone. + val cal = Calendar.getInstance() + cal.setTime(sqlDate) + Date(cal.get(Calendar.YEAR), Month(cal.get(Calendar.MONTH)), cal.get(Calendar.DAY_OF_MONTH)) + } + + private[database] def dateToSqlDate(date: Date): SqlDate = { + val cal = Calendar.getInstance() + cal.set(date.year, date.month, date.day, 0, 0, 0) + new SqlDate(cal.getTime.getTime) + } + + @deprecated("Dal is deprecated. Please use Repository trait instead!", "1.8.26") + type Dal = Repository + + @deprecated("SlickDal is deprecated. Please use SlickRepository class instead!", "1.8.26") + type SlickDal = SlickRepository + + @deprecated("FutureDal is deprecated. Please use FutureRepository class instead!", "1.8.26") + type FutureDal = FutureRepository +} |