Search code examples
scalaslickscala-cats

Scala, Slick, Cats - How to map different SQL errors with OptionT?


I have a simple slick query which run on database:

def method(): Future[Either[Error, MyCustomDTO]] = 
OptionT(database.run(query))
  .map(MyCustomDTO(_))
  .toRight(dataNotFound())
  .value

The problem is with .toRight. I would like to map it to sifferent errors depends on what was returned by database. E.g.

case FOREIGN_KEY_CONSTRAINT_VIOLATION.toString => constraintError()
case UNIQUE_CONSTRAINT_VIOLATION.toString => uniqueError()
case _ => dataNotFound()

I tried to do match case in .toRight(), but it does not work:

.toRight(error => error.asInstanceOf[PSQLException].getSQLState match { .... })

I'm wondering what is the best possibility to map different errors here in a correct way?


Solution

  • Take a look at the signature of toRight(...)

    def toRight[L](left: => L)(implicit F: Functor[F]): EitherT[F, L, A] =
      EitherT(cata(Left(left), Right.apply))
    

    both of these take by-name parameter - on other words they are special syntax of () => ... where () => ... in definition and ...() in application are inserted for you. Why? Because on toLeft/toRight in OptionT assume that you are handling the kind of error that is expresses by Option - that is None. Since there is no need to pass _: None.type => it uses by-name parameter instead.

    If you want to handle the error you have to handle it inside F[A] - by providing the right type class (ApplicativeError[F, Throwable]/MonadError[F, Throwable]) which would allow calling handleError/handleErrorWith/redeem etc

    // F: ApplicativeError[F, Throwable]
    // fa: F[MyCustomDTO]
    F.redeem(fa)(a => Right(a), error => Left(error match { ... }))
    
    // or with extension methods for AplicativeError
    fa.redeem(a => Right(a), error => Left(error match { ... }))
    
    // asRight is extension method creating Either
    fa.map(_.asRight[Error]).handleError(e => Left(...))
    

    since your F seem to be Future, and since you have it wrapped in OptionT I guess it worked better if you did something like:

    OptionT(database.run(query))
      .map(MyCustomDTO(_).asRight[Error])
      .getOrElse(dataNotFound().asLeft[MyCustomDTO])
      .handleError { error =>
        Left(error.asInstanceOf[PSQLException].getSQLState match {
          ...
        })
      }
    

    You could also give up on OptionT and do:

    database.run(query).attemptT // EitherT[Future, Throwable, ...]
      .leftMap { error =>
        error.asInstanceOf[PSQLException].getSQLState match {
          ...
        }
      }
      .subflatMap { option =>
        option.fold(dataNotFound().asLeft)(MyCustomDTO(_).asRight))
      }
      .value