From 4d172fab3381849cae49faaee6a9211724e29924 Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Wed, 25 Sep 2024 19:23:32 +0200 Subject: [PATCH 1/8] #13: QueryResultRow conversion for Product types * 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 --- .sbtrc | 2 + README.md | 63 +++++++- .../db/balta/classes/QueryResultRow.scala | 7 +- .../db/balta/implicits/MapImplicits.scala | 33 +++++ .../db/balta/implicits/OptionImplicits.scala | 32 +++++ .../implicits/QueryResultRowImplicits.scala | 91 ++++++++++++ .../db/mag/exceptions/NamingException.scala | 22 +++ .../co/absa/db/mag/naming/LettersCase.scala | 55 +++++++ .../absa/db/mag/naming/NamingConvention.scala | 54 +++++++ .../naming/implementations/AsIsNaming.scala | 50 +++++++ .../ExplicitNamingRequired.scala | 51 +++++++ .../implementations/MapBasedNaming.scala | 52 +++++++ .../implementations/SnakeCaseNaming.scala | 65 +++++++++ .../postgres/08_testing.simple_function.sql | 55 +++++++ .../implicits/MapImplicitsUnitTests.scala | 33 +++++ .../implicits/OptionImplicitsUnitTests.scala | 34 +++++ ...ryResultRowImplicitsIntegrationTests.scala | 136 ++++++++++++++++++ .../QueryResultRowImplicitsUnitTests.scala | 40 ++++++ .../db/mag/naming/LettersCaseUnitTests.scala | 33 +++++ .../implementations/AsIsNamingUnitTests.scala | 36 +++++ .../ExplicitNamingRequiredUnitTests.scala | 41 ++++++ .../MapBasedNamingUnitTests.scala | 69 +++++++++ .../SnakeCaseNamingUnitTests.scala | 53 +++++++ 23 files changed, 1101 insertions(+), 6 deletions(-) create mode 100644 balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala create mode 100644 balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala create mode 100644 balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/exceptions/NamingException.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/LettersCase.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/NamingConvention.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/implementations/AsIsNaming.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequired.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/implementations/MapBasedNaming.scala create mode 100644 balta/src/main/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNaming.scala create mode 100644 balta/src/test/resources/db/postgres/08_testing.simple_function.sql create mode 100644 balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/balta/implicits/OptionImplicitsUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/mag/naming/LettersCaseUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/mag/naming/implementations/AsIsNamingUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequiredUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/mag/naming/implementations/MapBasedNamingUnitTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNamingUnitTests.scala diff --git a/.sbtrc b/.sbtrc index 5f69dcb..944c0cb 100644 --- a/.sbtrc +++ b/.sbtrc @@ -25,3 +25,5 @@ alias test=; testOnly *UnitTests # * Integration tests alias testIT=; testOnly *IntegrationTests +# Run all tests +alias testAll=; testOnly * diff --git a/README.md b/README.md index e7ec7a4..58fb62d 100644 --- a/README.md +++ b/README.md @@ -27,17 +27,71 @@ It's a natural complement to the use of [Fa-Db library](https://github.com/AbsaO * The transaction start and rollback are done automatically before or after the execution respectively of the `test` function provided -Advantages of this approach is that the tests repeateble, they are isolated from each other and the database is always +Advantages of this approach is that the tests are repeateble, they are isolated from each other and the database is always in a known state before and after each test. -## How to Test -There are integration tests part of the package that can be run with the following command: +## How to write tests + +### [`DBTestSuite`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala) class + +The foundation is the [`DBTestSuite`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala) +class that provides the necessary setup and teardown for the tests to each run in its own transaction. It is an +enhancement class to standard ScalaTest `AnyFunSuite` class. + +Besides that, it provides easy access to tables, query them and insert data into them. + +And it allows easy access to database functions and executing them. + +### [`DBTable`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala) class + +Class for representing a database table. It provides methods for querying the table and inserting data into it. The class +instance is spawned by each `DBTestSuite.table` method call. + +### [`DBFunction`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala) class + +Class for representing a database function. It provides methods for calling the function and verifying the return values. +The class instance is spawned by each `DBTestSuite.function` method call. + +### [`QueryResult`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala) class + +This iterator represents the result of a query. It provides methods for iterating over the rows and columns of the result. + +### [`QueryResultRow`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala) class + +This class represents a row in a query result. It provides methods for accessing the columns of the row - via name or index. + +To make specific Postgres types available in `QueryResultRow` there is th implicit classes +[`Postgres.PostgresRow`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala) +enhancing the `QueryResultRow` with the ability to get type like `JSON` (including JSONB) and `JSON[]` (JSON array). + +There is also an implicit class +[`QueryResultImplicits.ProductTypeConvertor`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultImplicits.scala) +enhancing the `QueryResultRow` with the ability to convert the row to a product type (case class or tuple) via function +`toProductType[T]`. + + + + + +## How to test the library + +Use the `test` command to execute all unit tests, skipping all other types of tests. +```bash +sbt test +``` + +There are integration tests part of the package that can be run with the following command: ```bash sbt testIT ``` -The tests to finish successfully, a Postgres database must be running and populated. +If you want to run all tests, use the following command. +```bash +sbt testAll +``` + +The integrations tests to finish successfully, a Postgres database must be running and populated. * by default the database is expected to be running on `localhost:5432` * if you wish to run against a different server modify the `src/test/resources/database.properties` file * to populate the database run the scripts in the `src/test/resources/db/postgres` folder @@ -50,3 +104,4 @@ Please see [this file](RELEASE.md) for more details. ### Postgres * `TIMESTAMP WITH TIME ZONE[]`, `TIME WITH TIME ZONE[]`, generally arrays of time related types are not translated to appropriate time zone aware Scala/Java types +* Conversion to product type won't work if there are complex members in the product type like subclasses; with container types like `List`, it hasn't been tested \ No newline at end of file diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala index 337aceb..266911d 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala @@ -17,6 +17,7 @@ package za.co.absa.db.balta.classes import QueryResultRow._ +import za.co.absa.db.balta.implicits.MapImplicits.MapEnhancements import java.sql import java.sql.{Date, ResultSet, ResultSetMetaData, Time, Types} @@ -35,7 +36,10 @@ class QueryResultRow private[classes](val rowNumber: Int, private val columnNames: FieldNames) { def columnCount: Int = fields.length - def columnNumber(columnLabel: String): Int = columnNames(columnLabel.toLowerCase) + def columnNumber(columnLabel: String): Int = { + val actualLabel = columnLabel.toLowerCase + columnNames.getOrThrow(actualLabel, new NoSuchElementException(s"Column '$actualLabel' not found")) + } def apply(column: Int): Option[Object] = fields(column - 1) def apply(columnLabel: String): Option[Object] = apply(columnNumber(columnLabel)) @@ -119,7 +123,6 @@ class QueryResultRow private[classes](val rowNumber: Int, getAs(column: Int, transformerFnc _) } - } object QueryResultRow { diff --git a/balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala new file mode 100644 index 0000000..99d3726 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/MapImplicits.scala @@ -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) + } + } + +} diff --git a/balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala new file mode 100644 index 0000000..ec1d600 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/OptionImplicits.scala @@ -0,0 +1,32 @@ +/* + * 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 OptionImplicits { + implicit class OptionEnhancements[T](val option: Option[T]) extends AnyVal { + /** + * Gets the `option` value or throws the provided exception + * + * @param exception the exception to throw in case the `option` is None + * @return + */ + def getOrThrow(exception: => Throwable): T = { + option.getOrElse(throw exception) + } + } + +} diff --git a/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala new file mode 100644 index 0000000..87d9498 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala @@ -0,0 +1,91 @@ +/* + * 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 java.lang +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)) { + value + } else { + value.getOrThrow(new NullPointerException(s"Column '$columnLabel' is null")) + } + } + } +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/exceptions/NamingException.scala b/balta/src/main/scala/za/co/absa/db/mag/exceptions/NamingException.scala new file mode 100644 index 0000000..cf8101d --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/exceptions/NamingException.scala @@ -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) diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/LettersCase.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/LettersCase.scala new file mode 100644 index 0000000..e05a74e --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/LettersCase.scala @@ -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 + } +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/NamingConvention.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/NamingConvention.scala new file mode 100644 index 0000000..68245fa --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/NamingConvention.scala @@ -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 +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/AsIsNaming.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/AsIsNaming.scala new file mode 100644 index 0000000..e978139 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/AsIsNaming.scala @@ -0,0 +1,50 @@ +/* + * 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.implementations + +import za.co.absa.db.mag.naming.LettersCase.AsIs +import za.co.absa.db.mag.naming.{LettersCase, NamingConvention} + +/** + * `AsIsNaming` provides a naming convention that leaves strings as they are. + * It implements the [[NamingConvention]] trait. + * @param lettersCase - The case of the letters in the string. + */ +class AsIsNaming(lettersCase: LettersCase) extends NamingConvention { + + /** + * Returns the original string converted to the specified letter case. + * @param original - The original string. + * @return The original string converted to the specified letter case. + */ + override def stringPerConvention(original: String): String = { + lettersCase.convert(original) + } +} + +/** + * `AsIsNaming.Implicits` provides an implicit [[NamingConvention]] instance that leaves strings as they are. + */ +object AsIsNaming { + object Implicits { + + /** + * An implicit [[NamingConvention]] instance that leaves strings as they are. + */ + implicit val namingConvention: NamingConvention = new AsIsNaming(AsIs) + } +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequired.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequired.scala new file mode 100644 index 0000000..a1902d0 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequired.scala @@ -0,0 +1,51 @@ +/* + * 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.implementations + +import za.co.absa.db.mag.exceptions.NamingException +import za.co.absa.db.mag.naming.NamingConvention + +/** + * `ExplicitNamingRequired` is a `NamingConvention` that throws a `NamingException` for any string. + * This is used when explicit naming is required and no other naming convention should be applied. + */ +class ExplicitNamingRequired extends NamingConvention { + + /** + * Throws a `NamingConvention` with a message indicating that explicit naming is required. + * @param original - The original string. + * @return Nothing, as a `NamingException` is always thrown. + */ + override def stringPerConvention(original: String): String = { + val message = s"No convention for '$original', explicit naming required." + throw NamingException(message) + } +} + +/** + * `ExplicitNamingRequired.Implicits` provides an implicit `NamingConvention` instance that + * throws a `NamingException` for any string. + */ +object ExplicitNamingRequired { + object Implicits { + + /** + * An implicit `NamingConvention` instance that throws a `NamingException` for any string. + */ + implicit val namingConvention: NamingConvention = new ExplicitNamingRequired() + } +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/MapBasedNaming.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/MapBasedNaming.scala new file mode 100644 index 0000000..bcb40b6 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/MapBasedNaming.scala @@ -0,0 +1,52 @@ +/* + * 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.implementations + +import za.co.absa.db.balta.implicits.MapImplicits.MapEnhancements +import za.co.absa.db.mag.exceptions.NamingException +import za.co.absa.db.mag.naming.LettersCase.AsIs +import za.co.absa.db.mag.naming.{LettersCase, NamingConvention} + +/** + * `MapBasedNaming` requires an explicit map of name conversions provided in a form of a `Map[String, String]`. + * If the requested name is not found in the map, a `NamingException` is thrown. + */ +class MapBasedNaming private(names: Map[String, String], lettersCase: LettersCase) extends NamingConvention { + + /** + * Throws a `NamingConvention` if the original is not present between the keys of the Map. + * @param original - The original string. + * @return - The string from the map linked to the original string. + */ + override def stringPerConvention(original: String): String = { + names.getOrThrow(lettersCase.convert(original), NamingException(s"No convention for '$original' has been defined.")) + } +} + +object MapBasedNaming { + /** + * Creates a new `MapBasedNaming` instance with the specified names and letter cases. + * @param names - The map of names. + * @param keysLettersCase - The case of the keys in the map. Input values are converted to this case upon querying. + * @param valueLettersCase - The case of the values in the map. + * @return - The string from the map linked to the original string. + */ + def apply(names: Map[String, String], keysLettersCase: LettersCase = AsIs, valueLettersCase: LettersCase = AsIs): NamingConvention = { + val actualNames = names.map { case (k, v) => (keysLettersCase.convert(k), valueLettersCase.convert(v)) } + new MapBasedNaming(actualNames, keysLettersCase) + } +} diff --git a/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNaming.scala b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNaming.scala new file mode 100644 index 0000000..b534fe2 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNaming.scala @@ -0,0 +1,65 @@ +/* + * 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.implementations + +import za.co.absa.db.mag.naming.LettersCase.LowerCase +import za.co.absa.db.mag.naming.{LettersCase, NamingConvention} + +/** + * `SnakeCaseNaming` provides a naming convention that converts camel case strings to snake case. + * It implements the [[NamingConvention]] trait. + * + * @param lettersCase - The case of the letters in the string. + */ +class SnakeCaseNaming(lettersCase: LettersCase) extends NamingConvention { + + private def camelCaseToSnakeCase(s: String): String = { + s.replaceAll("([A-Z])", "_$1") + } + + private def stripIfFirstChar(s: String, ch: Char): String = { + if (s == "") { + s + } else if (s(0) == ch) { + s.substring(1) + } else { + s + } + } + + /** + * Converts the original string to snake case and the specified letter case. + * @param original - The original string. + * @return The original string converted to snake case and the specified letter case. + */ + override def stringPerConvention(original: String): String = { + lettersCase.convert(stripIfFirstChar(camelCaseToSnakeCase(original), '_')) + } +} + +/** + * `SnakeCaseNaming.Implicits` provides an implicit [[NamingConvention]] instance that converts camel case strings to snake case. + */ +object SnakeCaseNaming { + object Implicits { + + /** + * An implicit [[NamingConvention]] instance that converts camel case strings to snake case. + */ + implicit val namingConvention: NamingConvention = new SnakeCaseNaming(LowerCase) + } +} diff --git a/balta/src/test/resources/db/postgres/08_testing.simple_function.sql b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql new file mode 100644 index 0000000..bb1e504 --- /dev/null +++ b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql @@ -0,0 +1,55 @@ +/* + * 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. + */ + +CREATE OR REPLACE FUNCTION testing.simple_function( + IN i_return_data BOOLEAN, + OUT int_data INTEGER, + OUT text_data TEXT, + OUT timestamp_data TIMESTAMP WITH TIME ZONE, + OUT uuid_data UUID, + OUT ignored_data TEXT +) RETURNS record AS +$$ +------------------------------------------------------------------------------- +-- +-- Function: testing.simple_function(1) +-- Function returning static data for testing purposes +-- +-- Parameters: +-- i_return_data - flag if data re to be returned or not - +-- +-- Returns: +-- sample data +-- +------------------------------------------------------------------------------- +DECLARE +BEGIN + ignored_data := 'This is to be ignored'; + + IF i_return_data THEN + int_data := 42; + text_data := 'Hello World!'; + timestamp_data := '2023-01-01 00:00:00+00'::TIMESTAMP WITH TIME ZONE; + uuid_data := 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11'::UUID; + END IF; + + RETURN; +END; +$$ + LANGUAGE plpgsql VOLATILE + SECURITY DEFINER; + +GRANT EXECUTE ON FUNCTION testing.simple_function(BOOLEAN) TO mag_owner; diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala new file mode 100644 index 0000000..1e0902d --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala @@ -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 + +import org.scalatest.funsuite.AnyFunSuiteLike +import za.co.absa.db.balta.implicits.MapImplicits.MapEnhancements + + +class MapImplicitsUnitTests extends AnyFunSuiteLike { + private val map = Map("foo" -> 1, "bar" -> 2) + test("getOrThrow returns the value if it is defined") { + assert(map.getOrThrow("foo", new Exception("Foo")) == 1) + } + + test("getOrThrow throws an exception if the value is not defined") { + assertThrows[Exception](map.getOrThrow("barbar", new Exception("Foo"))) + } + +} diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/OptionImplicitsUnitTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/OptionImplicitsUnitTests.scala new file mode 100644 index 0000000..18e81e6 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/OptionImplicitsUnitTests.scala @@ -0,0 +1,34 @@ +/* + * 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 org.scalatest.funsuite.AnyFunSuiteLike +import za.co.absa.db.balta.implicits.OptionImplicits.OptionEnhancements + + +class OptionImplicitsUnitTests extends AnyFunSuiteLike { + test("getOrThrow returns the value if it is defined") { + val opt = Some(true) + assert(opt.getOrThrow(new Exception("Foo"))) + } + + test("getOrThrow throws an exception if the value is not defined") { + val opt = None + assertThrows[Exception](opt.getOrThrow(new Exception("Foo"))) + } + +} diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala new file mode 100644 index 0000000..922c179 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala @@ -0,0 +1,136 @@ +/* + * 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 java.time.OffsetDateTime +import java.util.UUID +import org.scalatest.funsuite.AnyFunSuiteLike +import za.co.absa.db.balta.classes.DBFunction +import za.co.absa.db.balta.implicits.QueryResultRowImplicits.ProductTypeConvertor +import za.co.absa.db.balta.testing.classes.DBTestingConnection +import za.co.absa.db.mag.naming.NamingConvention +import za.co.absa.db.mag.naming.implementations.MapBasedNaming + + +class QueryResultRowImplicitsIntegrationTests extends AnyFunSuiteLike with DBTestingConnection{ + private val function = DBFunction("testing.simple_function") + private val timestamp = OffsetDateTime.parse("2023-01-01T00:00:00Z") + private val uuid = UUID.fromString("a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11") + + test("Product type with Option types is created with actual values") { + import za.co.absa.db.mag.naming.implementations.SnakeCaseNaming.Implicits.namingConvention + + val result = function.setParam(true).execute{queryResult => + val row = queryResult.next() + row.toProductType[ResultTypeOptional] + } + val expected = ResultTypeOptional( + intData = Some(42), + textData = Some("Hello World!"), + timestampData = Some(timestamp), + uuidData = Some(uuid) + ) + assert(result == expected) + } + + test("Product type with Option types is created with no values") { + import za.co.absa.db.mag.naming.implementations.SnakeCaseNaming.Implicits.namingConvention + + val result = function.setParam(false).execute{queryResult => + val row = queryResult.next() + row.toProductType[ResultTypeOptional] + } + val expected = ResultTypeOptional( + None, None, None, None + ) + assert(result == expected) + } + + + test("Product type with no-Option types is create with values") { + import za.co.absa.db.mag.naming.implementations.SnakeCaseNaming.Implicits.namingConvention + + val result = function.setParam(true).execute{queryResult => + val row = queryResult.next() + row.toProductType[ResultType] + } + val expected = ResultType( + intData = 42, + textData = "Hello World!", + timestampData = timestamp, + uuidData = uuid + ) + assert(result == expected) + } + + test("Product type with no-Option types throws NullPointerException exception when no value encountered") { + import za.co.absa.db.mag.naming.implementations.SnakeCaseNaming.Implicits.namingConvention + + function.setParam(false).execute{queryResult => + val row = queryResult.next() + assertThrows[NullPointerException](row.toProductType[ResultType]) + } + } + + test("Product type with wrong names throws NoSuchElementException exception") { + import za.co.absa.db.mag.naming.implementations.SnakeCaseNaming.Implicits.namingConvention + + function.setParam(false).execute{queryResult => + val row = queryResult.next() + assertThrows[NoSuchElementException](row.toProductType[CaseClassOfWrongFields]) + } + } + + test("Product type of tuple is created if correct naming exists") { + implicit val naming: NamingConvention = MapBasedNaming(Map( + "_1" -> "int_data", + "_2" -> "text_data", + "_3" -> "timestamp_data", + "_4" -> "uuid_data" + )) + val result = function.setParam(true).execute{queryResult => + val row = queryResult.next() + row.toProductType[(Int, String, OffsetDateTime, UUID)] + } + val expected = ( + 42, + "Hello World!", + timestamp, + uuid + ) + assert(result == expected) + } +} + +// these classes has to be top level +case class ResultTypeOptional( + intData: Option[Int], + textData: Option[String], + timestampData: Option[OffsetDateTime], + uuidData: Option[UUID] + ) + +case class ResultType( + intData: Int, + textData: String, + timestampData: OffsetDateTime, + uuidData: UUID + ) + +case class CaseClassOfWrongFields( + notInTheRow: String + ) diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsUnitTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsUnitTests.scala new file mode 100644 index 0000000..2ac92fe --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsUnitTests.scala @@ -0,0 +1,40 @@ +/* + * 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 org.scalatest.funsuite.AnyFunSuiteLike + +import scala.reflect.runtime.universe.typeOf + +class QueryResultRowImplicitsUnitTests extends AnyFunSuiteLike { + + test("isOptionType should return true for Option type") { + assert(QueryResultRowImplicits.isOptionType(typeOf[Option[Int]])) + } + + test("isOptionType should return false for non-Option simple type") { + assert(!QueryResultRowImplicits.isOptionType(typeOf[Int])) + } + + test("isOptionType should return false for non-Option complex type") { + assert(!QueryResultRowImplicits.isOptionType(typeOf[this.type])) + } + + test("isOptionType should return false for non-Option container type") { + assert(!QueryResultRowImplicits.isOptionType(typeOf[List[String]])) + } +} diff --git a/balta/src/test/scala/za/co/absa/db/mag/naming/LettersCaseUnitTests.scala b/balta/src/test/scala/za/co/absa/db/mag/naming/LettersCaseUnitTests.scala new file mode 100644 index 0000000..c643fd2 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/mag/naming/LettersCaseUnitTests.scala @@ -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.mag.naming + +import org.scalatest.funsuite.AnyFunSuiteLike + +class LettersCaseUnitTests extends AnyFunSuiteLike { + test("AsIs") { + assert(LettersCase.AsIs.convert("Hello World!") == "Hello World!") + } + + test("LowerCase") { + assert(LettersCase.LowerCase.convert("Hello World!") == "hello world!") + } + + test("UpperCase") { + assert(LettersCase.UpperCase.convert("Hello World!") == "HELLO WORLD!") + } +} diff --git a/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/AsIsNamingUnitTests.scala b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/AsIsNamingUnitTests.scala new file mode 100644 index 0000000..b35b2ad --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/AsIsNamingUnitTests.scala @@ -0,0 +1,36 @@ +/* + * 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.implementations + +import org.scalatest.funsuite.AnyFunSuiteLike +import org.scalatest.matchers.should.Matchers +import za.co.absa.db.mag.naming.LettersCase + +class AsIsNamingUnitTests extends AnyFunSuiteLike with Matchers { + + val asIsNaming = new AsIsNaming(LettersCase.AsIs) + + test("AsIsNaming should return the same string") { + val input = "testString" + val expectedOutput = "testString" + + val output = asIsNaming.stringPerConvention(input) + + output shouldEqual expectedOutput + } + +} diff --git a/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequiredUnitTests.scala b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequiredUnitTests.scala new file mode 100644 index 0000000..e42a5f2 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/ExplicitNamingRequiredUnitTests.scala @@ -0,0 +1,41 @@ +/* + * 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.implementations + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import za.co.absa.db.mag.exceptions.NamingException + +class ExplicitNamingRequiredUnitTests extends AnyWordSpec with Matchers { + private val explicitNamingRequired = new ExplicitNamingRequired() + + "stringPerConvention" should { + "fail" in { + intercept[NamingException] { + explicitNamingRequired.stringPerConvention("") + } + } + } + + "fromClassNamePerConvention" should { + "fail" in { + intercept[NamingException] { + explicitNamingRequired.fromClassNamePerConvention(this) + } + } + } +} diff --git a/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/MapBasedNamingUnitTests.scala b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/MapBasedNamingUnitTests.scala new file mode 100644 index 0000000..8ee70dc --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/MapBasedNamingUnitTests.scala @@ -0,0 +1,69 @@ +/* + * 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.mag.naming.implementations + +import org.scalatest.funsuite.AnyFunSuiteLike +import za.co.absa.db.mag.exceptions.NamingException +import za.co.absa.db.mag.naming.LettersCase.{LowerCase, UpperCase} + +class MapBasedNamingUnitTests extends AnyFunSuiteLike { + private val map = Map( + "Hello" -> "World!", + "Foo" -> "Bar" + ) + + private val mapNamingConventionAsIs = MapBasedNaming(map) + private val mapNamingConventionLowerUpper = MapBasedNaming(map, LowerCase, UpperCase) + private val mapNamingConventionUpperLower = MapBasedNaming(map, UpperCase, LowerCase) + + test("MapBasedNaming with AsIs LetterCase should return the found string") { + + val input = "Hello" + val expectedOutput = "World!" + val output = mapNamingConventionAsIs.stringPerConvention(input) + + assert(output == expectedOutput) + } + + test("MapBasedNaming with altered case should return the found string in defined output case") { + val input1 = "Hello" + val expectedOutput1 = "WORLD!" + val output1 = mapNamingConventionLowerUpper.stringPerConvention(input1) + + assert(output1 == expectedOutput1) + + val input2 = "Foo" + val expectedOutput2 = "bar" + val output2 = mapNamingConventionUpperLower.stringPerConvention(input2) + + assert(output2 == expectedOutput2) + } + + test("MapBaseNaming fails when key is not found") { + val input = "NotInMap" + assertThrows[NamingException] { + mapNamingConventionAsIs.stringPerConvention(input) + } + } + + test("With default LetterCase MapBasedNaming cares about case") { + val input = "hello" + assertThrows[NamingException] { + mapNamingConventionAsIs.stringPerConvention(input) + } + } +} diff --git a/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNamingUnitTests.scala b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNamingUnitTests.scala new file mode 100644 index 0000000..fdd555d --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/mag/naming/implementations/SnakeCaseNamingUnitTests.scala @@ -0,0 +1,53 @@ +/* + * 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.implementations + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec +import za.co.absa.db.mag.naming.LettersCase.{AsIs, LowerCase, UpperCase} + +class SnakeCaseNamingUnitTests extends AnyWordSpec with Matchers { + private class ThisIsATestClass + private val testInstance = new ThisIsATestClass() + + "stringPerConvention" should { + "handle empty string" in { + val nm = new SnakeCaseNaming(AsIs) + nm.stringPerConvention("") should be("") + } + } + + "fromClassNamePerConvention" should { + "return snake case" when { + "requested as is" in { + val nm = new SnakeCaseNaming(AsIs) + val result = nm.fromClassNamePerConvention(testInstance) + result should be("This_Is_A_Test_Class") + } + "requested as lowercase" in { + val nm = new SnakeCaseNaming(LowerCase) + val result = nm.fromClassNamePerConvention(testInstance) + result should be("this_is_a_test_class") + } + "requested as upper case" in { + val nm = new SnakeCaseNaming(UpperCase) + val result = nm.fromClassNamePerConvention(testInstance) + result should be("THIS_IS_A_TEST_CLASS") + } + } + } +} From 77f32c66ae74dec381b3bd533b5334db4b27d3ba Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Wed, 25 Sep 2024 21:32:41 +0200 Subject: [PATCH 2/8] * added "simple function" into the build GitHub workflow --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f2982b7..9bfa1d9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,7 @@ jobs: psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/05_testing._base_types_data.sql psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/06_testing.pg_types.ddl psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql + psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/08_testing.simple_function.sql - name: Build and run integration tests run: sbt ++${{matrix.scala}} testIT From 31bbaa59a3449a7de4df0031d2e4bce0840d64d3 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Thu, 26 Sep 2024 16:43:53 +0200 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: miroslavpojer --- README.md | 2 +- .../test/resources/db/postgres/08_testing.simple_function.sql | 2 +- .../za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala | 1 + .../implicits/QueryResultRowImplicitsIntegrationTests.scala | 4 ++-- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 58fb62d..d4bf8e4 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ This iterator represents the result of a query. It provides methods for iteratin This class represents a row in a query result. It provides methods for accessing the columns of the row - via name or index. -To make specific Postgres types available in `QueryResultRow` there is th implicit classes +To make specific Postgres types available in `QueryResultRow` there is the implicit class [`Postgres.PostgresRow`](https://github.com/AbsaOSS/balta/blob/master/balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala) enhancing the `QueryResultRow` with the ability to get type like `JSON` (including JSONB) and `JSON[]` (JSON array). diff --git a/balta/src/test/resources/db/postgres/08_testing.simple_function.sql b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql index bb1e504..3541a7c 100644 --- a/balta/src/test/resources/db/postgres/08_testing.simple_function.sql +++ b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql @@ -29,7 +29,7 @@ $$ -- Function returning static data for testing purposes -- -- Parameters: --- i_return_data - flag if data re to be returned or not - +-- i_return_data - flag if data are to be returned or not - -- -- Returns: -- sample data diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala index 1e0902d..62607ae 100644 --- a/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/MapImplicitsUnitTests.scala @@ -22,6 +22,7 @@ import za.co.absa.db.balta.implicits.MapImplicits.MapEnhancements class MapImplicitsUnitTests extends AnyFunSuiteLike { private val map = Map("foo" -> 1, "bar" -> 2) + test("getOrThrow returns the value if it is defined") { assert(map.getOrThrow("foo", new Exception("Foo")) == 1) } diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala index 922c179..65e8b0d 100644 --- a/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicitsIntegrationTests.scala @@ -96,7 +96,7 @@ class QueryResultRowImplicitsIntegrationTests extends AnyFunSuiteLike with DBTes } test("Product type of tuple is created if correct naming exists") { - implicit val naming: NamingConvention = MapBasedNaming(Map( + implicit val naming: NamingConvention = MapBasedNaming(Map( "_1" -> "int_data", "_2" -> "text_data", "_3" -> "timestamp_data", @@ -116,7 +116,7 @@ class QueryResultRowImplicitsIntegrationTests extends AnyFunSuiteLike with DBTes } } -// these classes has to be top level +// these classes have to be top level case class ResultTypeOptional( intData: Option[Int], textData: Option[String], From 66538abce2c3561b22ba23f1a604902c92347119 Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Thu, 26 Sep 2024 23:30:19 +0200 Subject: [PATCH 4/8] * removed unused import --- .../za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala index 87d9498..8535611 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/QueryResultRowImplicits.scala @@ -20,7 +20,6 @@ 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 java.lang import scala.reflect.runtime.currentMirror import scala.reflect.runtime.universe._ From 136369a9dfc1fba62c5b19434139ff5cc7d62183 Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Sun, 29 Sep 2024 06:18:24 +0200 Subject: [PATCH 5/8] * updated JaCoCo workflow --- .github/workflows/jacoco_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/jacoco_report.yml b/.github/workflows/jacoco_report.yml index 2ad5cf8..01af369 100644 --- a/.github/workflows/jacoco_report.yml +++ b/.github/workflows/jacoco_report.yml @@ -46,6 +46,7 @@ jobs: psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/05_testing._base_types_data.sql psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/06_testing.pg_types.ddl psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql + psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/08_testing.simple_function.sql - name: Build and run tests continue-on-error: true From 0b319a44d1041858ce1000aaf62db6bd292ac683 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:22:50 +0200 Subject: [PATCH 6/8] Apply suggestions from code review Co-authored-by: Ladislav Sulak --- README.md | 4 ++-- .../test/resources/db/postgres/08_testing.simple_function.sql | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc520c1..2edf469 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ It's a natural complement to the use of [Fa-Db library](https://github.com/AbsaO * The transaction start and rollback are done automatically before or after the execution respectively of the `test` function provided -Advantages of this approach is that the tests are repeateble, they are isolated from each other and the database is always +Advantages of this approach is that the tests are repeatable, they are isolated from each other and the database is always in a known state before and after each test. @@ -39,7 +39,7 @@ The foundation is the [`DBTestSuite`](https://github.com/AbsaOSS/balta/blob/mast class that provides the necessary setup and teardown for the tests to each run in its own transaction. It is an enhancement class to standard ScalaTest `AnyFunSuite` class. -Besides that, it provides easy access to tables, query them and insert data into them. +Besides that, it provides easy access to tables, queries them and inserts data into them. And it allows easy access to database functions and executing them. diff --git a/balta/src/test/resources/db/postgres/08_testing.simple_function.sql b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql index 3541a7c..c5ec58a 100644 --- a/balta/src/test/resources/db/postgres/08_testing.simple_function.sql +++ b/balta/src/test/resources/db/postgres/08_testing.simple_function.sql @@ -29,7 +29,7 @@ $$ -- Function returning static data for testing purposes -- -- Parameters: --- i_return_data - flag if data are to be returned or not - +-- i_return_data - flag if data are to be returned or not -- -- Returns: -- sample data From fa0d06df860144458bb2f0c32cd7a3ea8de000c3 Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Thu, 3 Oct 2024 17:08:33 +0200 Subject: [PATCH 7/8] * improved documentation --- .../co/absa/db/balta/classes/QueryResultRow.scala | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala index d2108d4..2d9f5d4 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala @@ -27,20 +27,23 @@ import java.util.UUID /** * This is a row of a query result. It allows to safely extract values from the row by column name. * - * @param rowNumber - the number of the row in the result set - * @param fields - the values of the row - * @param columnNames - the names of the columns + * @param rowNumber - the number of the row in the result set + * @param fields - the values of the row + * @param columnLabels - the names of the columns; class uses `columnLabel(s)` to refer to the column names in accordance + * to `java.sql.ResultSet`, which is is build around and that despite that aliases are not expected + * to appear here */ class QueryResultRow private[classes](val rowNumber: Int, private val fields: Vector[Option[Object]], - private val columnNames: FieldNames) { + private val columnLabels: FieldNames) { def columnCount: Int = fields.length def columnNumber(columnLabel: String): Int = { val actualLabel = columnLabel.toLowerCase - columnNames.getOrThrow(actualLabel, new NoSuchElementException(s"Column '$actualLabel' not found")) + columnLabels.getOrThrow(actualLabel, new NoSuchElementException(s"Column '$actualLabel' not found")) } + val r: ResultSet = null /** * Extracts a value from the row by column number. * @param column - the number of the column, 1 based From f7cbfd007cf801c3ede6367cc5e8f0dd68288a9d Mon Sep 17 00:00:00 2001 From: David Benedeki Date: Fri, 4 Oct 2024 13:08:24 +0200 Subject: [PATCH 8/8] * removed forgotten code --- .../main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala index 2d9f5d4..ad0ab5a 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala @@ -43,7 +43,6 @@ class QueryResultRow private[classes](val rowNumber: Int, columnLabels.getOrThrow(actualLabel, new NoSuchElementException(s"Column '$actualLabel' not found")) } - val r: ResultSet = null /** * Extracts a value from the row by column number. * @param column - the number of the column, 1 based