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

QueryResultRow conversion for Product types #13

Closed
salamonpavel opened this issue Nov 27, 2023 · 1 comment · Fixed by #36
Closed

QueryResultRow conversion for Product types #13

salamonpavel opened this issue Nov 27, 2023 · 1 comment · Fixed by #36
Labels
enhancement New feature or request

Comments

@salamonpavel
Copy link
Collaborator

salamonpavel commented Nov 27, 2023

Background

At the moment it's difficult to create test assertions of the db results given the fact that one needs to extract fields from the QueryResultRow object. I propose to enhance capabilities of the library to allow for automatic instance creation for product types.

Feature

Balta will be able to create instances of product types from QueryResultRow

Example

class GetActorByIdTest extends DBTestSuite {

  test("Get actor by id - actor with given id exists EXISTING") {
    val expectedActor = Actor(actorId = 1, firstName = "Malin", lastName = "Akerman")

    function("runs.get_actor_by_id")
      .setParam("i_actor_id", 1)
      .execute { queryResult =>
        assert(queryResult.hasNext)
        val row = queryResult.next()
        assert(row.getInt("actor_id").get == expectedActor.actorId)
        assert(row.getString("first_name").get == expectedActor.firstName)
        assert(row.getString("last_name").get == expectedActor.lastName)
        assert(!queryResult.hasNext)
      }
  }

  import za.co.absa.balta.classes.DefaultColumnNameMappers.SnakeCaseForCamelCaseMapper

  test("Get actor by id - actor with given id exists PROPOSAL") {
    val expectedActor = Actor(actorId = 1, firstName = "Malin", lastName = "Akerman")

    function("runs.get_actor_by_id")
      .setParam("i_actor_id", 1)
      .execute { queryResult =>
        assert(queryResult.hasNext)
        val row = queryResult.next()
        val actualActor = row.toProductType[Actor]
        assert(expectedActor == actualActor)
        assert(!queryResult.hasNext)
      }
  }

}

Proposed Solution for Discussion

An example of possible solution (needs to be tested for various scenarios, only tested on the test example above)

package za.co.absa.balta.classes

import org.postgresql.util.PGobject

import java.sql.{Date, ResultSet, Time}
import java.time.{Instant, OffsetDateTime}
import java.util.UUID

import scala.reflect.runtime.universe._
import scala.reflect.runtime.currentMirror

class QueryResultRow private[classes](val resultSet: ResultSet) extends AnyVal {

  // existing code not included

  def toProductType[T <: Product : TypeTag](implicit mapper: ColumnNameMapper): T = {
    val tpe = typeOf[T]
    val classSymbol = tpe.typeSymbol.asClass
    val classMirror = currentMirror.reflectClass(classSymbol)
    val constructorSymbol = tpe.decl(termNames.CONSTRUCTOR)
    val defaultConstructor =
      if (constructorSymbol.isMethod) constructorSymbol.asMethod
      else {
        val ctors = constructorSymbol.asTerm.alternatives
        ctors.map(_.asMethod).find(_.isPrimaryConstructor).get
      }
    val constructorMirror = classMirror.reflectConstructor(defaultConstructor)
    val params = defaultConstructor.paramLists.flatten.map { param =>
      val name = param.name.decodedName.toString
      val columnName = mapper.map(name)
      val value = resultSet.getObject(columnName)
      if (resultSet.wasNull()) null else value
    }
    constructorMirror(params: _*).asInstanceOf[T]
  }

}

trait ColumnNameMapper {
  def map(name: String): String
}

object DefaultColumnNameMappers {

  // when database columns use the same names as product type
  implicit object IdentityMapper extends ColumnNameMapper {
    def map(name: String): String = name
  }

  // when database columns use upper case and product type uses lower case
  implicit object UpperCaseForLowerCaseMapper extends ColumnNameMapper {
    def map(name: String): String = name.toUpperCase
  }

  // when database columns use lower case and product type uses upper case
  implicit object LowerCaseForUpperCaseMapper extends ColumnNameMapper {
    def map(name: String): String = name.toLowerCase
  }

  // when database columns use snake_case and product type uses camelCase
  implicit object SnakeCaseForCamelCaseMapper extends ColumnNameMapper {
    def map(name: String): String = {
      val regex = "([a-z])([A-Z]+)".r
      val replacement = "$1_$2"
      regex.replaceAllIn(name, replacement).toLowerCase
    }
  }

  // when database columns use kebab-case and product type uses camelCase
  implicit object KebabCaseForCamelCaseMapper extends ColumnNameMapper {
    def map(name: String): String = {
      val regex = "([a-z])([A-Z]+)".r
      val replacement = "$1-$2"
      regex.replaceAllIn(name, replacement).toLowerCase
    }
  }

}
@salamonpavel salamonpavel added the enhancement New feature or request label Nov 27, 2023
@lsulak
Copy link
Collaborator

lsulak commented Dec 11, 2023

I didn't try it, but I like the proposal!

benedeki added a commit that referenced this issue Sep 25, 2024
* implicit class that adds the ability to convert `QueryResultRow` to a product type
* implicit classes that add `getOrThrow` methods to `Option` and `Map`
* copied `NamingConvention` classes from Fa-DB
* added `sbt testAll` alias
* enhanced the README.md to include some basic classes of the library used for DB testing
benedeki added a commit that referenced this issue Oct 4, 2024
* implicit class that adds the ability to convert `QueryResultRow` to a product type
* implicit classes that add `getOrThrow` methods to `Option` and `Map`
* copied `NamingConvention` classes from Fa-DB
* added `sbt testAll` alias
* enhanced the README.md to include some basic classes of the library used for DB testing

---------

Co-authored-by: miroslavpojer <miroslav.pojer@absa.africa>
Co-authored-by: Ladislav Sulak <laco.sulak@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

2 participants