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
1 change: 0 additions & 1 deletion .github/workflows/scala.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ jobs:
strategy:
matrix:
include:
- scala-version: 2.12.x
- scala-version: 2.13.x
steps:
- uses: actions/checkout@v2
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ val `unicorn-core` = project
.settings(Settings.core *)
.settings(
libraryDependencies ++= Dependencies.core,
libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value,
// cannot be higher due to tests not able to reproduce abnormal DB behavior
coverageMinimum := 100,
Test / scalastyleConfig := file("scalastyle-test-config.xml"),
Expand Down
8 changes: 4 additions & 4 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import sbt._
object Dependencies {

val mainCore: Seq[ModuleID] = Seq(
"com.typesafe.slick" %% "slick" % "3.4.1",
"com.typesafe.slick" %% "slick" % "3.6.1",
"joda-time" % "joda-time" % "2.14.0",
"org.joda" % "joda-convert" % "3.0.1"
)
Expand All @@ -17,12 +17,12 @@ object Dependencies {
val core: Seq[ModuleID] = mainCore ++ testCore

val mainPlay: Seq[ModuleID] = Seq(
"com.typesafe.play" %% "play-slick" % "5.1.0"
"com.typesafe.play" %% "play-slick" % "5.4.0"
)

val testPlay: Seq[ModuleID] = Seq(
"org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test,
"com.typesafe.play" %% "play-test" % "2.8.20" % "test"
"org.scalatestplus.play" %% "scalatestplus-play" % "6.0.2" % Test,
"com.typesafe.play" %% "play-test" % "2.9.8" % "test"
)

val play: Seq[ModuleID] = mainPlay ++ testPlay
Expand Down
3 changes: 1 addition & 2 deletions project/Settings.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import sbtrelease.ReleasePlugin.autoImport._

object Settings {

val scala_2_12 = "2.12.20"
val scala_2_13 = "2.13.16"

val alsoOnTest = "compile->compile;test->test"
Expand All @@ -13,7 +12,7 @@ object Settings {
val common = Seq(
organization := "org.virtuslab",
scalaVersion := scala_2_13,
crossScalaVersions := Seq(scala_2_12, scala_2_13),
crossScalaVersions := Seq(scala_2_13),
releaseCrossBuild := true,

Test / fork := true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package org.virtuslab.unicorn

import slick.lifted.MappedTo

/**
* Base trait for implementing ids.
* It is existential trait so it can have only defs.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.virtuslab.unicorn

import slick.jdbc.JdbcProfile

import scala.reflect.ClassTag

Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class lacks documentation explaining its purpose and usage. Consider adding a scaladoc comment describing how it provides column type conversion for isomorphic types.

Suggested change
/**
* Provides column type conversion for isomorphic types in Slick.
*
* This class enables the mapping of two types that are isomorphic (i.e., they can be converted
* back and forth without loss of information) to be used as column types in a database schema.
*
* @param profile The Slick `JdbcProfile` used for database interaction.
* @example
* Given an isomorphism between types `A` and `B`, you can define a column type for `A`:
* {{{
* implicit val columnTypeForA: BaseColumnType[A] =
* new IsomorphicColumnTypeConversion().isomorphicType[A, B]
* }}}
*/

Copilot uses AI. Check for mistakes.
class IsomorphicColumnTypeConversion(implicit val profile: JdbcProfile) {
import profile._
implicit def isomorphicType[A, B](implicit iso: Isomorphism[A, B], ct: ClassTag[A], jt: BaseColumnType[B]): BaseColumnType[A] =
MappedColumnType.base[A, B](iso.map, iso.comap)
}
54 changes: 54 additions & 0 deletions unicorn-core/src/main/scala/org/virtuslab/unicorn/MappedTo.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.virtuslab.unicorn

import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
import scala.util.control.NonFatal

/** An isomorphism between two types that can be used for mapped column types. */
class Isomorphism[A, B](val map: A => B, val comap: B => A)

trait MappedToBase extends Any {
type Underlying
def value: Underlying
}

object MappedToBase {
implicit def mappedToIsomorphism[E <: MappedToBase]: Isomorphism[E, E#Underlying] = macro mappedToIsomorphismMacroImpl[E]

def mappedToIsomorphismMacroImpl[E <: MappedToBase](c: Context)(implicit e: c.WeakTypeTag[E]): c.Expr[Isomorphism[E, E#Underlying]] = {
import c.universe._
// Check that E <: MappedToBase. Due to SI-8351 the macro can be expanded before scalac has
// checked this. The error message here will never be seen because scalac's subsequent bounds
// check fails, overriding our error (or backtracking in implicit search).
if (!(e.tpe <:< c.typeOf[MappedToBase]))
c.abort(c.enclosingPosition, "Work-around for SI-8351 leading to illegal macro-invocation -- You should not see this message")
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message indicates this is a workaround and states users should not see it, which could be confusing if they do encounter it. Consider providing a more helpful error message.

Suggested change
c.abort(c.enclosingPosition, "Work-around for SI-8351 leading to illegal macro-invocation -- You should not see this message")
c.abort(c.enclosingPosition, "Invalid macro invocation: The type parameter must extend MappedToBase. Please ensure that your type definition satisfies this requirement.")

Copilot uses AI. Check for mistakes.
implicit val eutag = c.TypeTag[E#Underlying](e.tpe.member(TypeName("Underlying")).typeSignatureIn(e.tpe))
val cons = c.Expr[E#Underlying => E](Function(
List(ValDef(Modifiers(Flag.PARAM), TermName("v"), /*Ident(eu.tpe.typeSymbol)*/ TypeTree(), EmptyTree)),
Apply(
Select(New(TypeTree(e.tpe)), termNames.CONSTRUCTOR),
List(Ident(TermName("v"))))))
val res = reify { new Isomorphism[E, E#Underlying](_.value, cons.splice) }
try c.typecheck(res.tree) catch {
case NonFatal(ex) =>
val p = c.enclosingPosition
val msg = "Error typechecking MappedTo expansion: " + ex.getMessage
println(p.source.path + ":" + p.line + ": " + msg)
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using println for error reporting in library code is not recommended. Consider using a proper logging framework or removing this debug statement.

Copilot uses AI. Check for mistakes.
c.error(c.enclosingPosition, msg)
}
res
}
}

/**
* The base type for automatically mapped column types.
* Extending this type (with a type parameter ``T`` which is already a
* supported column type) lets you use your custom type as a column
* type in the Lifted Embedding. You must provide a constructor that
* takes a single value of the underlying type (same restriction as
* for value classes).
*/
trait MappedTo[T] extends Any with MappedToBase {
type Underlying = T
def value: T
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ trait BaseTest[Underlying] extends AnyFlatSpecLike with Matchers with BeforeAndA

val dbDriver = "org.h2.Driver"

lazy val DB: unicorn.profile.backend.DatabaseDef = unicorn.profile.backend.Database.forURL(dbURL, driver = dbDriver)
lazy val DB: unicorn.profile.backend.JdbcDatabaseDef = unicorn.profile.backend.Database.forURL(dbURL, driver = dbDriver)

implicit val defaultPatience: PatienceConfig =
PatienceConfig(timeout = Span(5, Seconds), interval = Span(500, Millis))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ trait UUIDTable extends UUIDTestUnicorn {

import unicorn._

case class UniqueUserId(id: UUID) extends BaseId[UUID]
val ictc = new IsomorphicColumnTypeConversion
import ictc._

case class UniqueUserId(id: UUID) extends BaseId[UUID] with MappedToBase

case class PersonRow(id: Option[UniqueUserId], name: String) extends WithId[UUID, UniqueUserId]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@ package org.virtuslab.unicorn.repositories

import org.virtuslab.unicorn.LongUnicornIdentifiers.IdCompanion
import org.virtuslab.unicorn.TestUnicorn.profile.api._
import org.virtuslab.unicorn.{ BaseId, BaseTest, LongTestUnicorn }
import org.virtuslab.unicorn.{ BaseId, BaseTest, IsomorphicColumnTypeConversion, LongTestUnicorn, MappedToBase }

import scala.concurrent.ExecutionContext.Implicits.global

class JunctionRepositoryTest extends BaseTest[Long] with LongTestUnicorn {

import unicorn._

val ictc = new IsomorphicColumnTypeConversion
import ictc._

behavior of classOf[JunctionRepository[_, _, _]].getSimpleName

case class OrderId(id: Long) extends BaseId[Long]
case class OrderId(id: Long) extends BaseId[Long] with MappedToBase

object OrderId extends IdCompanion[OrderId]

case class CustomerId(id: Long) extends BaseId[Long]
case class CustomerId(id: Long) extends BaseId[Long] with MappedToBase

object CustomerId extends IdCompanion[CustomerId]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ trait AbstractUserTable {
import unicorn.profile.api._
import identifiers._

case class UserId(id: Long) extends BaseId[Long]
val ictc = new IsomorphicColumnTypeConversion
import ictc._

case class UserId(id: Long) extends BaseId[Long] with MappedToBase

object UserId extends CoreCompanion[UserId]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ abstract class UnicornPlayLike[Underlying](dbConfig: DatabaseConfig[JdbcProfile]

val profile: JdbcProfile = dbConfig.profile

val db: JdbcBackend#DatabaseDef = dbConfig.db
val db: JdbcBackend#Database = dbConfig.db
}

abstract class UnicornPlay[Underlying](dbConfig: DatabaseConfig[JdbcProfile])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ object StringUnicornPlayIdentifiers extends PlayIdentifiersImpl[String] {

import StringUnicornPlayIdentifiers._

case class UserId(id: String) extends BaseId[String]
case class UserId(id: String) extends BaseId[String] with MappedToBase

object UserId extends IdCompanion[UserId]

Expand All @@ -28,6 +28,9 @@ trait UserQuery {
import unicorn._
import unicorn.profile.api._

val ictc = new IsomorphicColumnTypeConversion
import ictc._

class UserTable(tag: SlickTag) extends IdTable[UserId, UserRow](tag, "test") {
def name = column[String]("name")
override def * : ProvenShape[UserRow] = (id.?, name) <> (UserRow.tupled, UserRow.unapply)
Expand Down