-
Notifications
You must be signed in to change notification settings - Fork 0
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
#13: QueryResultRow conversion for Product types #36
Merged
benedeki
merged 13 commits into
master
from
feature/13-queryresultrow-conversion-for-product-types
Oct 4, 2024
Merged
Changes from 9 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4d172fa
#13: QueryResultRow conversion for Product types
benedeki 77f32c6
* added "simple function" into the build GitHub workflow
benedeki 31bbaa5
Apply suggestions from code review
benedeki 66538ab
* removed unused import
benedeki 5c3fca8
Merge branch 'master' into feature/13-queryresultrow-conversion-for-p…
benedeki e61c4a9
Merge branch 'master' into feature/13-queryresultrow-conversion-for-p…
benedeki 136369a
* updated JaCoCo workflow
benedeki 0b319a4
Apply suggestions from code review
benedeki 6abf5fa
Merge branch 'master' into feature/13-queryresultrow-conversion-for-p…
benedeki fa0d06d
* improved documentation
benedeki 8c9fcd4
Merge branch 'master' into feature/13-queryresultrow-conversion-for-p…
benedeki 87177d8
Merge branch 'feature/13-queryresultrow-conversion-for-product-types'…
benedeki f7cbfd0
* removed forgotten code
benedeki File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
33 changes: 33 additions & 0 deletions
33
balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright 2023 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.db.balta.implicits | ||
|
||
object MapImplicits { | ||
implicit class MapEnhancements[K, V](val map: Map[K, V]) extends AnyVal { | ||
/** | ||
* Gets the value associated with the key or throws the provided exception | ||
* @param key - the key to get the value for | ||
* @param exception - the exception to throw in case the `option` is None | ||
* @tparam V1 - the type of the value | ||
* @return - the value associated with key if it exists, otherwise throws the provided exception | ||
*/ | ||
def getOrThrow[V1 >: V](key: K, exception: => Throwable): V1 = { | ||
map.getOrElse(key, throw exception) | ||
} | ||
} | ||
|
||
} |
90 changes: 90 additions & 0 deletions
90
balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
/* | ||
* Copyright 2023 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.db.balta.implicits | ||
|
||
import za.co.absa.db.balta.classes.QueryResultRow | ||
import za.co.absa.db.balta.implicits.OptionImplicits.OptionEnhancements | ||
import za.co.absa.db.mag.naming.NamingConvention | ||
|
||
import scala.reflect.runtime.currentMirror | ||
import scala.reflect.runtime.universe._ | ||
|
||
object QueryResultRowImplicits { | ||
|
||
def isOptionType(typeToCheck: Type): Boolean = { | ||
typeToCheck <:< typeOf[Option[_]] | ||
} | ||
|
||
/** | ||
* This class provides an implicit conversion from QueryResultRow to a case class | ||
* This logic placed in an implicit class to prevent polluting the QueryResultRow class with too much unrelated logic | ||
* @param row The QueryResultRow to convert | ||
*/ | ||
implicit class ProductTypeConvertor(val row: QueryResultRow) extends AnyVal { | ||
|
||
/** | ||
* Converts a QueryResultRow to a case class | ||
* @param namingConvention - The naming convention to use when converting field names to column names | ||
* @tparam T - The case class to convert to | ||
* @return - The case class instance filled with data from the QueryResultRow | ||
*/ | ||
def toProductType[T <: Product : TypeTag](implicit namingConvention: NamingConvention): T = { | ||
val tpe = typeOf[T] | ||
val defaultConstructor = getConstructor(tpe) | ||
val constructorMirror = getConstructorMirror(tpe, defaultConstructor) | ||
val params = readParamsFromRow(defaultConstructor) | ||
constructorMirror(params: _*).asInstanceOf[T] | ||
} | ||
|
||
private def getConstructor(tpe: Type): MethodSymbol = { | ||
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 | ||
} | ||
defaultConstructor | ||
} | ||
|
||
private def getConstructorMirror(tpe: Type, constructor: MethodSymbol): MethodMirror = { | ||
val classSymbol = tpe.typeSymbol.asClass | ||
val classMirror = currentMirror.reflectClass(classSymbol) | ||
val constructorMirror = classMirror.reflectConstructor(constructor) | ||
constructorMirror | ||
} | ||
|
||
private def readParamsFromRow(constructor: MethodSymbol)(implicit namingConvention: NamingConvention): List[Any] = { | ||
constructor.paramLists.flatten.map { param => | ||
val name = param.name.decodedName.toString | ||
val paramType = param.typeSignature | ||
val columnLabel = namingConvention.stringPerConvention(name) | ||
getParamValue(columnLabel, paramType) | ||
} | ||
|
||
} | ||
|
||
private def getParamValue[T: TypeTag](columnLabel: String, expectedType: Type): Any = { | ||
val value = row(columnLabel) | ||
if (isOptionType(expectedType)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here, deciding how to pass in the actual value to the constructor. |
||
value | ||
} else { | ||
value.getOrThrow(new NullPointerException(s"Column '$columnLabel' is null")) | ||
} | ||
} | ||
} | ||
} |
22 changes: 22 additions & 0 deletions
22
balta/src/main/scala/za/co/absa/db/mag/exceptions/NamingException.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright 2022 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.db.mag.exceptions | ||
|
||
/** | ||
* Exception thrown when a naming convention is not found for a given string | ||
*/ | ||
case class NamingException(message: String) extends Exception(message) |
55 changes: 55 additions & 0 deletions
55
balta/src/main/scala/za/co/absa/db/mag/naming/LettersCase.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright 2022 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.db.mag.naming | ||
|
||
/** | ||
* `LettersCase` is a sealed trait that represents different cases of letters. | ||
* It provides a method to convert a string to the specific case. | ||
*/ | ||
sealed trait LettersCase { | ||
|
||
/** | ||
* Converts a string to the specific case. | ||
* @param s - The original string. | ||
* @return The string converted to the specific case. | ||
*/ | ||
def convert(s: String): String | ||
} | ||
|
||
object LettersCase { | ||
|
||
/** | ||
* `AsIs` is a [[LettersCase]] that leaves strings as they are. | ||
*/ | ||
case object AsIs extends LettersCase { | ||
override def convert(s: String): String = s | ||
} | ||
|
||
/** | ||
* `LowerCase` is a [[LettersCase]] that converts strings to lower case. | ||
*/ | ||
case object LowerCase extends LettersCase { | ||
override def convert(s: String): String = s.toLowerCase | ||
} | ||
|
||
/** | ||
* `UpperCase` is a [[LettersCase]] that converts strings to upper case. | ||
*/ | ||
case object UpperCase extends LettersCase { | ||
override def convert(s: String): String = s.toUpperCase | ||
} | ||
} |
54 changes: 54 additions & 0 deletions
54
balta/src/main/scala/za/co/absa/db/mag/naming/NamingConvention.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* Copyright 2022 ABSA Group Limited | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package za.co.absa.db.mag.naming | ||
|
||
/** | ||
* `NamingConvention` is a base trait that defines the interface for different naming conventions. | ||
* It provides methods to convert a class name according to given naming convention. | ||
*/ | ||
trait NamingConvention { | ||
|
||
/** | ||
* Converts the class name according to the specific naming convention. | ||
* @param c - The class. | ||
* @return The class name converted to string according to the specific naming convention. | ||
*/ | ||
def fromClassNamePerConvention(c: Class[_]): String = { | ||
val className = c.getSimpleName | ||
val cleanClassName = className.lastIndexOf('$') match { | ||
case -1 => className | ||
case x => className.substring(0, x) | ||
} | ||
stringPerConvention(cleanClassName) | ||
} | ||
|
||
/** | ||
* Converts the class name according to the specific naming convention. | ||
* @param instance - The instance of the class. | ||
* @return The class name converted to string according to the specific naming convention. | ||
*/ | ||
def fromClassNamePerConvention(instance: AnyRef): String = { | ||
fromClassNamePerConvention(instance.getClass) | ||
} | ||
|
||
/** | ||
* Converts the original string according to the specific naming convention. | ||
* @param original - The original string. | ||
* @return The original string converted according to the specific naming convention. | ||
*/ | ||
def stringPerConvention(original: String): String | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why
Label
terminology?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Original terminology from JDBC
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but it's column name right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I read here that it's this: https://bugs.mysql.com/bug.php?id=35610
so
alias
orcolumn name
. Not sure if in our case it's always only column name?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look at the
ResultSet
class methods.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm I still don't fully know the answer if you think deeper into it (i.e. whether both can be applied to our case or only column name) :D
https://docs.oracle.com/javase/7/docs/api/java/sql/ResultSet.html
E.g.
Okay, we can follow it. But maybe at least document it super briefly? That this terminology is coming from X with some link perhaps etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Basically, if we are using an underlying technology A that is using special terminology B for something that uses X and Y, and X is known thing - in our case column name - and if we are not using Y, then it makes sense not to follow A's way by using B, just use X; special-ish undocumented terms are never a good thing in my opinion)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you please suggest the documentation text. I am not sure I follow.