Skip to content

Commit

Permalink
#118: adding status info into Right - always retrievable
Browse files Browse the repository at this point in the history
  • Loading branch information
lsulak committed Mar 28, 2024
1 parent bed8a55 commit a577c3d
Show file tree
Hide file tree
Showing 17 changed files with 77 additions and 61 deletions.
16 changes: 12 additions & 4 deletions core/src/main/scala/za/co/absa/fadb/DBEngine.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package za.co.absa.fadb
import cats.Monad
import cats.implicits.toFunctorOps
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatusWithData

import scala.language.higherKinds

Expand Down Expand Up @@ -50,7 +51,7 @@ abstract class DBEngine[F[_]: Monad] {
* @tparam R - return type of the query
* @return - result of database query with status
*/
protected def runWithStatus[R](query: QueryWithStatusType[R]): F[Seq[Either[StatusException, R]]]
protected def runWithStatus[R](query: QueryWithStatusType[R]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]]

/**
* Public method to execute when query is expected to return multiple results
Expand All @@ -62,7 +63,8 @@ abstract class DBEngine[F[_]: Monad] {
* @return - sequence of the results of database query
*/
def fetchAll[R](query: QueryType[R]): F[Seq[R]] = run(query)
def fetchAllWithStatus[R](query: QueryWithStatusType[R]): F[Seq[Either[StatusException, R]]] = runWithStatus(query)
def fetchAllWithStatus[R](query: QueryWithStatusType[R]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]] =
runWithStatus(query)

/**
* Public method to execute when query is expected to return exactly one row
Expand All @@ -74,7 +76,7 @@ abstract class DBEngine[F[_]: Monad] {
* @return - sequence of the results of database query
*/
def fetchHead[R](query: QueryType[R]): F[R] = run(query).map(_.head)
def fetchHeadWithStatus[R](query: QueryWithStatusType[R]): F[Either[StatusException, R]] =
def fetchHeadWithStatus[R](query: QueryWithStatusType[R]): F[DBEngine.ExceptionOrStatusWithData[R]] =
runWithStatus(query).map(_.head)

/**
Expand All @@ -87,6 +89,12 @@ abstract class DBEngine[F[_]: Monad] {
* @return - sequence of the results of database query
*/
def fetchHeadOption[R](query: QueryType[R]): F[Option[R]] = run(query).map(_.headOption)
def fetchHeadOptionWithStatus[R](query: QueryWithStatusType[R]): F[Option[Either[StatusException, R]]] =
def fetchHeadOptionWithStatus[R](query: QueryWithStatusType[R]): F[Option[DBEngine.ExceptionOrStatusWithData[R]]] =
runWithStatus(query).map(_.headOption)
}

case object DBEngine {

type ExceptionOrStatusWithData[R] = Either[StatusException, FunctionStatusWithData[R]]

}
18 changes: 9 additions & 9 deletions core/src/main/scala/za/co/absa/fadb/DBFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package za.co.absa.fadb

import cats.MonadError
import cats.implicits.toFlatMapOps
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatusWithData
import za.co.absa.fadb.status.handling.StatusHandling

import scala.language.higherKinds
Expand Down Expand Up @@ -109,25 +109,24 @@ abstract class DBFunctionWithStatus[I, R, E <: DBEngine[F], F[_]](functionNameOv
* @param values - The values to pass over to the database function.
* @return - A sequence of results from the database function.
*/
protected def multipleResults(values: I)(implicit me: MonadError[F, Throwable]): F[Seq[Either[StatusException, R]]] =
protected def multipleResults(values: I)(implicit me: MonadError[F, Throwable]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]] =
query(values).flatMap(q => dBEngine.fetchAllWithStatus(q))

/**
* Executes the database function and returns a single result.
* @param values - The values to pass over to the database function.
* @return - A single result from the database function.
*/
protected def singleResult(values: I)(implicit me: MonadError[F, Throwable]): F[Either[StatusException, R]] =
protected def singleResult(values: I)(implicit me: MonadError[F, Throwable]): F[DBEngine.ExceptionOrStatusWithData[R]] =
query(values).flatMap(q => dBEngine.fetchHeadWithStatus(q))

/**
* Executes the database function and returns an optional result.
* @param values - The values to pass over to the database function.
* @return - An optional result from the database function.
*/
protected def optionalResult(values: I)(implicit me: MonadError[F, Throwable]): F[Option[Either[StatusException, R]]] = {
protected def optionalResult(values: I)(implicit me: MonadError[F, Throwable]): F[Option[DBEngine.ExceptionOrStatusWithData[R]]] =
query(values).flatMap(q => dBEngine.fetchHeadOptionWithStatus(q))
}

/**
* The fields to select from the database function call
Expand All @@ -148,7 +147,7 @@ abstract class DBFunctionWithStatus[I, R, E <: DBEngine[F], F[_]](functionNameOv
protected def query(values: I)(implicit me: MonadError[F, Throwable]): F[dBEngine.QueryWithStatusType[R]]

// To be provided by an implementation of QueryStatusHandling
override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, A]
override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): DBEngine.ExceptionOrStatusWithData[A]
}

object DBFunction {
Expand Down Expand Up @@ -245,7 +244,7 @@ object DBFunction {
* @return - a sequence of values, each coming from a row returned from the DB function transformed to scala
* type `R` wrapped around with Either, providing StatusException if raised
*/
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[Seq[Either[StatusException, R]]] =
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]] =
multipleResults(values)
}

Expand All @@ -271,7 +270,8 @@ object DBFunction {
* @return - the value returned from the DB function transformed to scala type `R`
* wrapped around with Either, providing StatusException if raised
*/
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[Either[StatusException, R]] = singleResult(values)
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[DBEngine.ExceptionOrStatusWithData[R]] =
singleResult(values)
}

/**
Expand All @@ -296,7 +296,7 @@ object DBFunction {
* @return - the value returned from the DB function transformed to scala type `R` if a row is returned,
* otherwise `None`, wrapped around with Either, providing StatusException if raised
*/
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[Option[Either[StatusException, R]]] =
def apply(values: I)(implicit me: MonadError[F, Throwable]): F[Option[DBEngine.ExceptionOrStatusWithData[R]]] =
optionalResult(values)
}

Expand Down
11 changes: 6 additions & 5 deletions core/src/main/scala/za/co/absa/fadb/Query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

package za.co.absa.fadb

import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatusWithData

/**
* The basis for all query types of [[DBEngine]] implementations
Expand Down Expand Up @@ -44,14 +44,15 @@ trait QueryWithStatus[A, B, R] {
* @param statusWithData - the status with data
* @return either a status exception or the data
*/
def toStatusExceptionOrData(statusWithData: FunctionStatusWithData[B]): Either[StatusException, R]
def toStatusExceptionOrData(statusWithData: FunctionStatusWithData[B]): DBEngine.ExceptionOrStatusWithData[R]

/**
* Returns the result of the query or a status exception
* @param initialResult - the initial result of the query
* @return the result of the query or a status exception
*/
def getResultOrException(initialResult: A): Either[StatusException, R] = toStatusExceptionOrData(
processStatus(initialResult)
)
def getResultOrException(initialResult: A): DBEngine.ExceptionOrStatusWithData[R] =
toStatusExceptionOrData(
processStatus(initialResult)
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@
* limitations under the License.
*/

package za.co.absa.fadb

import za.co.absa.fadb.status.FunctionStatus
package za.co.absa.fadb.status

/**
* Represents a function status with data.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@

package za.co.absa.fadb.status.handling

import za.co.absa.fadb.FunctionStatusWithData
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatusWithData

/**
* `StatusHandling` is a base trait that defines the interface for handling the status of a function invocation.
Expand All @@ -28,7 +28,8 @@ trait StatusHandling {
/**
* Checks the status of a function invocation.
* @param statusWithData - The status of the function invocation with data.
* @return Either a `StatusException` if the status code indicates an error, or the data if the status code is successful.
* @return Either a `StatusException` if the status code indicates an error, or the data (along with the status
* information so that it's retrievable) if the status code is successful.
*/
def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, A]
def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, FunctionStatusWithData[A]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

package za.co.absa.fadb.status.handling.implementations

import za.co.absa.fadb.FunctionStatusWithData
import za.co.absa.fadb.DBEngine
import za.co.absa.fadb.exceptions._
import za.co.absa.fadb.status.FunctionStatusWithData
import za.co.absa.fadb.status.handling.StatusHandling

/**
Expand All @@ -29,10 +30,10 @@ trait StandardStatusHandling extends StatusHandling {
/**
* Checks the status of a function invocation.
*/
override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, A] = {
override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): DBEngine.ExceptionOrStatusWithData[A] = {
val functionStatus = statusWithData.functionStatus
functionStatus.statusCode / 10 match {
case 1 => Right(statusWithData.data)
case 1 => Right(statusWithData)
case 2 => Left(ServerMisconfigurationException(functionStatus))
case 3 => Left(DataConflictException(functionStatus))
case 4 => Left(DataNotFoundException(functionStatus))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@

package za.co.absa.fadb.status.handling.implementations

import za.co.absa.fadb.FunctionStatusWithData
import za.co.absa.fadb.exceptions.{OtherStatusException, StatusException}
import za.co.absa.fadb.DBEngine
import za.co.absa.fadb.exceptions.OtherStatusException
import za.co.absa.fadb.status.FunctionStatusWithData
import za.co.absa.fadb.status.handling.StatusHandling

/**
Expand All @@ -26,9 +27,9 @@ import za.co.absa.fadb.status.handling.StatusHandling
trait UserDefinedStatusHandling extends StatusHandling {
def OKStatuses: Set[Integer]

override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, A] =
override def checkStatus[A](statusWithData: FunctionStatusWithData[A]): DBEngine.ExceptionOrStatusWithData[A] =
if (OKStatuses.contains(statusWithData.functionStatus.statusCode)) {
Right(statusWithData.data)
Right(statusWithData)
} else {
Left(OtherStatusException(statusWithData.functionStatus))
}
Expand Down
3 changes: 1 addition & 2 deletions core/src/test/scala/za/co/absa/fadb/DBFunctionSuite.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import cats.MonadError
import cats.implicits._
import org.scalatest.funsuite.AnyFunSuite
import za.co.absa.fadb.DBFunction.DBSingleResultFunction
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.naming.implementations.SnakeCaseNaming.Implicits.namingConvention

import scala.concurrent.ExecutionContext.Implicits.global
Expand All @@ -34,7 +33,7 @@ class DBFunctionSuite extends AnyFunSuite {

class EngineThrow extends DBEngine[Future] {
override def run[R](query: QueryType[R]): Future[Seq[R]] = neverHappens
override def runWithStatus[R](query: QueryWithStatusType[R]): Future[Seq[Either[StatusException, R]]] = neverHappens
override def runWithStatus[R](query: QueryWithStatusType[R]): Future[Seq[DBEngine.ExceptionOrStatusWithData[R]]] = neverHappens
}

private object FooNamed extends DBSchema
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ package za.co.absa.fadb.status.handling.implementations

import org.scalatest.funsuite.AnyFunSuiteLike
import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper
import za.co.absa.fadb.FunctionStatusWithData
import za.co.absa.fadb.exceptions._
import za.co.absa.fadb.status.FunctionStatus
import za.co.absa.fadb.status.{FunctionStatus, FunctionStatusWithData}

class StandardStatusHandlingTest extends AnyFunSuiteLike {

Expand All @@ -31,7 +30,7 @@ class StandardStatusHandlingTest extends AnyFunSuiteLike {
val functionStatus = FunctionStatus(statusCode, "Success")
val statusWithData = FunctionStatusWithData(functionStatus, "Data")
val result = standardQueryStatusHandling.checkStatus(statusWithData)
result shouldBe Right("Data")
result shouldBe Right(statusWithData)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class DoobieMultipleResultFunctionWithStatusTest extends AnyFunSuite with Doobie
val results = getActorsByLastname(GetActorsByLastnameQueryParameters("Simpson", Some("Liza"))).unsafeRunSync()
val actualData = results.map {
case Left(_) => fail("should not be left")
case Right(value) => value
case Right(value) => value.data
}

assert(actualData.length == 1)
Expand All @@ -74,7 +74,7 @@ class DoobieMultipleResultFunctionWithStatusTest extends AnyFunSuite with Doobie
val results = getActorsByLastname(GetActorsByLastnameQueryParameters("Simpson")).unsafeRunSync()
val actualData = results.map {
case Left(_) => fail("should not be left")
case Right(value) => value
case Right(value) => value.data
}

assert(actualData.length == 1)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ class DoobieSingleResultFunctionWithStatusTest extends AnyFunSuite with DoobieTe
val result = errorIfNotOne(1).unsafeRunSync()
result match {
case Left(_) => fail("should not be left")
case Right(value) => assert(value.contains(1))
case Right(value) => assert(value.data.contains(1))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import doobie.implicits._
import doobie.util.Read
import za.co.absa.fadb.DBEngine
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatusWithData

import scala.language.higherKinds

Expand Down Expand Up @@ -55,13 +56,16 @@ class DoobieEngine[F[_]: Async](val transactor: Transactor[F]) extends DBEngine[
/**
* Executes a Doobie query and returns the result as an `F[Either[StatusException, Seq[R]]]`.
*
* Note: `StatusWithData` is needed here because it is more 'flat' in comparison to `FunctionStatusWithData`
* and Doobie's `Read` wasn't able to work with it.
*
* @param query the Doobie query to execute
* @param readStatusWithDataR the `Read[StatusWithData[R]]` instance used to read the query result into `StatusWithData[R]`
* @return the query result
*/
private def executeQueryWithStatus[R](
query: QueryWithStatusType[R]
)(implicit readStatusWithDataR: Read[StatusWithData[R]]): F[Seq[Either[StatusException, R]]] = {
)(implicit readStatusWithDataR: Read[StatusWithData[R]]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]] = {
query.fragment.query[StatusWithData[R]].to[Seq].transact(transactor).map(_.map(query.getResultOrException))
}

Expand All @@ -80,7 +84,7 @@ class DoobieEngine[F[_]: Async](val transactor: Transactor[F]) extends DBEngine[
* @param query the Doobie query to run
* @return the query result as an `F[Either[StatusException, R]]`
*/
override def runWithStatus[R](query: QueryWithStatusType[R]): F[Seq[Either[StatusException, R]]] = {
override def runWithStatus[R](query: QueryWithStatusType[R]): F[Seq[DBEngine.ExceptionOrStatusWithData[R]]] = {
executeQueryWithStatus(query)(query.readStatusWithDataR)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import doobie.implicits.toSqlInterpolator
import doobie.util.Read
import doobie.util.fragment.Fragment
import za.co.absa.fadb.DBFunction._
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.{DBSchema, FunctionStatusWithData}
import za.co.absa.fadb.{DBEngine, DBSchema}
import za.co.absa.fadb.status.FunctionStatusWithData

import scala.language.higherKinds

Expand Down Expand Up @@ -196,7 +196,7 @@ trait DoobieFunctionWithStatus[I, R, F[_]] extends DoobieFunctionBase[R] {
}

// This is to be mixed in by an implementation of StatusHandling
def checkStatus[A](statusWithData: FunctionStatusWithData[A]): Either[StatusException, A]
def checkStatus[A](statusWithData: FunctionStatusWithData[A]): DBEngine.ExceptionOrStatusWithData[A]
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ package za.co.absa.fadb.doobie

import doobie.util.Read
import doobie.util.fragment.Fragment
import za.co.absa.fadb.exceptions.StatusException
import za.co.absa.fadb.status.FunctionStatus
import za.co.absa.fadb.{FunctionStatusWithData, Query, QueryWithStatus}
import za.co.absa.fadb.status.{FunctionStatus, FunctionStatusWithData}
import za.co.absa.fadb.{DBEngine, Query, QueryWithStatus}

/**
* `DoobieQuery` is a class that extends `Query` with `R` as the result type.
Expand All @@ -41,7 +40,7 @@ class DoobieQuery[R](val fragment: Fragment)(implicit val readR: Read[R]) extend
*/
class DoobieQueryWithStatus[R](
val fragment: Fragment,
checkStatus: FunctionStatusWithData[R] => Either[StatusException, R]
checkStatus: FunctionStatusWithData[R] => DBEngine.ExceptionOrStatusWithData[R]
)(implicit val readStatusWithDataR: Read[StatusWithData[R]])
extends QueryWithStatus[StatusWithData[R], R, R] {

Expand All @@ -58,6 +57,6 @@ class DoobieQueryWithStatus[R](
* @param statusWithData - the status with data
* @return either a status exception or the data
*/
override def toStatusExceptionOrData(statusWithData: FunctionStatusWithData[R]): Either[StatusException, R] =
override def toStatusExceptionOrData(statusWithData: FunctionStatusWithData[R]): DBEngine.ExceptionOrStatusWithData[R] =
checkStatus(statusWithData)
}
Loading

0 comments on commit a577c3d

Please sign in to comment.