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

65 Views Asked by At

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?

1

There are 1 best solutions below

2
Mateusz Kubuszok On BEST ANSWER

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