Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce MonadError & refactor Doobie classes #113

Merged
merged 11 commits into from
Feb 21, 2024

Conversation

salamonpavel
Copy link
Contributor

@salamonpavel salamonpavel commented Jan 28, 2024

In the provided code, MonadError is used to catch exceptions that might be thrown when constructing the SQL query. If an exception is thrown, it is caught and represented as a failure in the F context. MonadError is a type class in Scala that abstracts over data types which define a flatMap operation and can signal errors. This includes types like Option, Either, Try, Future, IO, etc.

Apart from introduction of MonadError into both Slick and Doobie module, the Doobie functions have undergone a further significant refactoring process. Users have to provide function value from input to sequence of Doobie Fragments during the construction phase, thereby ensuring that no code can escape from the computational context F. This not only enhances the safety and predictability of the code but also eliminates a substantial amount of boilerplate. The ability to concatenate Doobie Fragments has facilitated this transformation, enabling flexible and dynamic construction of SQL queries.

However, this refactoring strategy could not be directly applied to the Slick module due to the lack of support for concatenation in SQLActionBuilder. This limitation necessitates the implementation of the sql method within the class body when using Slick.

An example of code throwing exception that's not captured in the computational context. Notice that the Fragment is constructed outside of computational IO context.

  class GetAllDateTimeTypes(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
      extends DoobieSingleResultFunction[Int, DatesTimes, IO] {

    override def sql(values: Int)(implicit read: Read[DatesTimes]): Fragment =
      throw new Exception("boom!")
      sql"SELECT * FROM ${Fragment.const(functionName)}($values)"
  }

An example where the error is captured within its context. Notice the simplification and reduction of boilerplate code and the fact that function from input type to Seq[Fragment] is needed to be passed to the constructor.

class GetAllDateTimeTypes(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
      extends DoobieSingleResultFunction[Int, DatesTimes, IO](values => Seq(fr"$values"))

The function passed to the constructor is of this signature.

def toFragmentsSeq: I => Seq[Fragment]

The sql method is already implemented in terms of meSql and made final. toFragmentsSeq is evaluated within the MonadError context. Exceptions can't escape.

protected final def sql(values: I)(implicit read: Read[R], ME: MonadError[F, Throwable]): F[Fragment] = {
    meSql(values, selectEntry, functionName, alias)(read, ME)
  }

private def meSql(values: I, selectEntry: String, functionName: String, alias: String)(implicit
    read: Read[R],
    ME: MonadError[F, Throwable]
  ): F[Fragment] = {
    ME.catchNonFatal {
      val fragments = toFragmentsSeq(values)
      val args = fragments.toList match {
        case head :: tail => tail.foldLeft(head)((acc, frag) => acc ++ fr"," ++ frag)
        case Nil          => fr""
      }
      sql"SELECT ${Fragment.const(selectEntry)} FROM ${Fragment.const(functionName)}($args) ${Fragment.const(alias)};"
    }
  }

In Slick module the similar is accomplished in this way. User still needs to implement sql method in the classes body. meSql then wraps this code in the computational context. Since the sql method is passed by-name, potential exceptions won't escape. From user point of view the Slick api hasn't changed.

protected def sql(values: I): SQLActionBuilder

  protected final def meSql(
    sqlActionBuilder: => SQLActionBuilder
  )(implicit ME: MonadError[Future, Throwable]): Future[SQLActionBuilder] = {
    ME.catchNonFatal(sqlActionBuilder)
  }

Now back to Doobie module....
Given that the arguments to the sql function are passed as a function this opens door for functional composition and reusability.

  implicit def toFragmentsFunctionSemigroup[T]: Semigroup[T => Seq[Fragment]] = {
    (f1: T => Seq[Fragment], f2: T => Seq[Fragment]) => (params: T) => f1(params) ++ f2(params)
  }

  private val firstNameFragment: GetActorsQueryParameters => Seq[Fragment] = params => Seq(fr"${params.firstName}")
  private val lastNameFragment: GetActorsQueryParameters => Seq[Fragment] = params => Seq(fr"${params.lastName}")

  // concatenation of function results after their evaluation
  private val combinedQueryFragments: GetActorsQueryParameters => Seq[Fragment] =
    params => firstNameFragment(params) ++ lastNameFragment(params)

  // using Semigroup's combine method, |+| is syntactical sugar for combine method
  private val combinedUsingSemigroup = firstNameFragment |+| lastNameFragment

  // not combined, defined as one function
  private val getActorsQueryFragments: GetActorsQueryParameters => Seq[Fragment] = {
    values => Seq(fr"${values.firstName}", fr"${values.lastName}")
  }

  // and notice how the boilerplate code was eliminated
  class GetActors(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
      extends DoobieMultipleResultFunction[GetActorsQueryParameters, Actor, IO](combinedUsingSemigroup)

Fixes #114

@salamonpavel salamonpavel added the work in progress Work on this item is not yet finished (mainly intended for PRs) label Jan 28, 2024
Copy link

github-actions bot commented Jan 28, 2024

JaCoCo code coverage report - scala 2.12.17

Total Project Coverage 27.05% 🍏
Module Coverage
fa-db:core Jacoco Report - scala:2.12.17 60.28%
fa-db:slick Jacoco Report - scala:2.12.17 0%
fa-db:doobie Jacoco Report - scala:2.12.17 0%
Files
Module File Coverage [1.67%]
fa-db:core Jacoco Report - scala:2.12.17 DBFunction.scala 6.25%
fa-db:slick Jacoco Report - scala:2.12.17 SlickFunction.scala 0%
SlickQuery.scala 0%
fa-db:doobie Jacoco Report - scala:2.12.17 DoobieFunction.scala 0%

Copy link

github-actions bot commented Jan 28, 2024

JaCoCo code coverage report - scala 2.13.12

Total Project Coverage 27.94% 🍏
Module Coverage
fa-db:core Jacoco Report - scala:2.13.12 61.8%
fa-db:slick Jacoco Report - scala:2.13.12 0%
fa-db:doobie Jacoco Report - scala:2.13.12 0%
Files
Module File Coverage [1.7%]
fa-db:core Jacoco Report - scala:2.13.12 DBFunction.scala 6.48%
fa-db:slick Jacoco Report - scala:2.13.12 SlickFunction.scala 0%
SlickQuery.scala 0%
fa-db:doobie Jacoco Report - scala:2.13.12 DoobieFunction.scala 0%

@salamonpavel salamonpavel changed the title Feature/monaderror and boilerplate Introduce MonadError & refactor Doobie classes Jan 28, 2024
@salamonpavel salamonpavel removed the work in progress Work on this item is not yet finished (mainly intended for PRs) label Jan 29, 2024
@salamonpavel salamonpavel marked this pull request as ready for review January 29, 2024 06:37
Copy link

@jakipatryk jakipatryk left a comment

Choose a reason for hiding this comment

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

Just quickly went through the code. Overall the idea LGTM, especially the Doobie part. However, I didn't do a deep review of the internal implementation (I can't now).

@@ -47,29 +49,33 @@ abstract class DBFunction[I, R, E <: DBEngine[F], F[_]](functionNameOverride: Op
* @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): F[Seq[R]] = dBEngine.fetchAll(query(values))
protected def multipleResults(values: I)(implicit ME: MonadError[F, Throwable]): F[Seq[R]] =

Choose a reason for hiding this comment

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

Why ME not me? I saw some other place with implicit lowercase, so probably makes sense to be me instead.
(same in other methods)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok, will change everywhere to lower case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

import org.scalatest.funsuite.AnyFunSuite
import za.co.absa.fadb.DBSchema
import za.co.absa.fadb.doobie.DoobieFunction.DoobieMultipleResultFunction

class DoobieMultipleResultFunctionTest extends AnyFunSuite with DoobieTest {

class GetActors(implicit schema: DBSchema, dbEngine: DoobieEngine[IO])
extends DoobieMultipleResultFunction[GetActorsQueryParameters, Actor, IO] {
// implicit def toFragmentsFunctionSemigroup[T]: Semigroup[T => Seq[Fragment]] = {

Choose a reason for hiding this comment

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

What is this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a left over code, will remove it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@salamonpavel
Copy link
Contributor Author

@lsulak @jakipatryk
Thanks for the initial review. I have addressed your comments. Additionally I have noticed there was a code duplication in fragments composition - I have extracted the duplicated code into composeFragments method in DoobieFunctionBase. Please let me know if you see any other issues. If not please add your approvals so we can start building on top of this code.

@miroslavpojer
Copy link
Contributor

miroslavpojer commented Feb 5, 2024

  • pulled
  • test run - Failed
  • local jacoco run - Failed by test run && 0% code coverage of new module 'doobie' detected
  • GH jacoco post - Failed the posts do not provide all available information

Test run
command: sbt test failing with output
[error] /Users/ab024ll/repos/absa/branches/fa-db-113/slick/src/main/scala/za/co/absa/fadb/slick/support/PgUUIDSupport.scala:32:5: Symbol 'type com.typesafe.config.Config' is missing from the classpath. [error] This symbol is required by 'method slick.basic.BasicProfile.loadProfileConfig'. [error] Make sure that type Config is in your classpath and check for conflicting dependencies with-Ylog-classpath. [error] A full rebuild may help if 'BasicProfile.class' was compiled against an incompatible version of com.typesafe.config. [error] driver match { [error] ^ [error] one error found

GH jacoco post
In file build.yml there is missing , in line 96.

@salamonpavel
Copy link
Contributor Author

  • pulled

    • test run - Failed

    • local jacoco run - Failed by test run && 0% code coverage of new module 'doobie' detected

    • GH jacoco post - Failed the posts do not provide all available information

Test run command: sbt test failing with output [error] /Users/ab024ll/repos/absa/branches/fa-db-113/slick/src/main/scala/za/co/absa/fadb/slick/support/PgUUIDSupport.scala:32:5: Symbol 'type com.typesafe.config.Config' is missing from the classpath. [error] This symbol is required by 'method slick.basic.BasicProfile.loadProfileConfig'. [error] Make sure that type Config is in your classpath and check for conflicting dependencies with-Ylog-classpath. [error] A full rebuild may help if 'BasicProfile.class' was compiled against an incompatible version of com.typesafe.config. [error] driver match { [error] ^ [error] one error found

GH jacoco post In file build.yml there is missing , in line 96.

added the missing comma
sbt test command runs successfully, there must be a problem on your side

@miroslavpojer
Copy link
Contributor

miroslavpojer commented Feb 6, 2024

  • pulled

    • test run - Failed
    • local jacoco run - Failed by test run && 0% code coverage of new module 'doobie' detected
    • GH jacoco post - Failed the posts do not provide all available information

Test run command: sbt test failing with output [error] /Users/ab024ll/repos/absa/branches/fa-db-113/slick/src/main/scala/za/co/absa/fadb/slick/support/PgUUIDSupport.scala:32:5: Symbol 'type com.typesafe.config.Config' is missing from the classpath. [error] This symbol is required by 'method slick.basic.BasicProfile.loadProfileConfig'. [error] Make sure that type Config is in your classpath and check for conflicting dependencies with-Ylog-classpath. [error] A full rebuild may help if 'BasicProfile.class' was compiled against an incompatible version of com.typesafe.config. [error] driver match { [error] ^ [error] one error found
GH jacoco post In file build.yml there is missing , in line 96.

added the missing comma sbt test command runs successfully, there must be a problem on your side

  • pulled
  • test run - Fixed - Passing
  • local jacoco run - Fixed - still 0% coverage of Doobie module
  • GH jacoco post - Fixed - now it provide useful output information

@miroslavpojer
Copy link
Contributor

miroslavpojer commented Feb 6, 2024

@salamonpavel Create please doobie/README.md file and describe the steps needed to run it tests for doobie module.
I would like to keep this knowledge here for later INT env works.
Thanks

@salamonpavel
Copy link
Contributor Author

salamonpavel commented Feb 6, 2024

@salamonpavel Create please doobie/README.md file and describe the steps needed to run it tests for doobie module. I would like to keep this knowledge here for later INT env works. Thanks

@miroslavpojer
For execution of integration tests in the doobie module (in fact for any module) you need to manually deploy all sql scripts that these tests need to your local database instance and configure a connection to it.
For the doobie module, the sql scripts can be found in doobie/src/it/database and the connection is hardcoded in DoobieTest trait.

@miroslavpojer
Copy link
Contributor

miroslavpojer commented Feb 6, 2024

@salamonpavel Create please doobie/README.md file and describe the steps needed to run it tests for doobie module. I would like to keep this knowledge here for later INT env works. Thanks

@miroslavpojer For execution of integration tests in the doobie module (in fact for any module) you need to manually deploy all sql scripts that these tests need to your local database instance and configure a connection to it. For the doobie module, the sql scripts can be found in doobie/src/it/database and the connection is hardcoded in DoobieTest trait.

Created Issue - #116 to finish found problem with manual Integration test run - short doc & missing test data expected by tests.

doobie/src/it/README.md Outdated Show resolved Hide resolved
slick/src/it/README.md Outdated Show resolved Hide resolved
@@ -16,6 +16,7 @@

package za.co.absa.fadb.doobie

import cats.Monad
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this actually used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch

functionNameOverride: Option[String] = None
)(implicit override val schema: DBSchema,
)(implicit
override val schema: DBSchema,
val dbEngine: DoobieEngine[F],
val readR: Read[R],
val readSelectWithStatus: Read[StatusWithData[R]]
Copy link
Collaborator

Choose a reason for hiding this comment

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

I like the formatting of these.

Btw, some code docs have very long lines, can you improve that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, shortened the docs.

Copy link
Collaborator

@lsulak lsulak left a comment

Choose a reason for hiding this comment

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

I finished with the review, very interesting - the idea behind all these fragments and the way it's used

@salamonpavel salamonpavel merged commit 8ff2a10 into master Feb 21, 2024
7 checks passed
@salamonpavel salamonpavel deleted the feature/monaderror-and-boilerplate branch February 21, 2024 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Capture potential errors in its computational context using MonadError type class
4 participants