From 8778d5fb11bde0d6b2a0f1424cdc76c83079424a Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Fri, 8 Dec 2023 16:34:46 +0100 Subject: [PATCH] #7: Move the code from atum-service into this repository (#10) * moved the code * added code description and enhanced `README.md` * fixed for Scala 2.13 --------- Co-authored-by: miroslavpojer --- .editorconfig | 34 +++ .gitattributes | 28 +++ .github/workflows/assign_issue_to_project.yml | 2 +- .github/workflows/dependent_items.yml | 2 +- .gitignore | 54 +++++ README.md | 22 +- .../scala/za/co/absa/balta/DBTestSuite.scala | 172 +++++++++++++++ .../za/co/absa/balta/classes/DBFunction.scala | 173 +++++++++++++++ .../absa/balta/classes/DBQuerySupport.scala | 43 ++++ .../za/co/absa/balta/classes/DBTable.scala | 204 ++++++++++++++++++ .../co/absa/balta/classes/QueryResult.scala | 47 ++++ .../absa/balta/classes/QueryResultRow.scala | 97 +++++++++ .../za/co/absa/balta/classes/package.scala | 42 ++++ .../classes/setter/AllowedParamTypes.scala | 64 ++++++ .../balta/classes/setter/CustomDBType.scala | 25 +++ .../co/absa/balta/classes/setter/Params.scala | 157 ++++++++++++++ .../absa/balta/classes/setter/SetterFnc.scala | 94 ++++++++ .../za/co/absa/balta/implicits/package.scala | 31 +++ build.sbt | 35 +++ project/Dependencies.scala | 34 +++ project/build.properties | 1 + 21 files changed, 1358 insertions(+), 3 deletions(-) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/package.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala create mode 100644 balta/src/main/scala/za/co/absa/balta/implicits/package.scala create mode 100644 build.sbt create mode 100644 project/Dependencies.scala create mode 100644 project/build.properties diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8617271 --- /dev/null +++ b/.editorconfig @@ -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. + +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +trim_trailing_whitespace = true + +[*.{java,scala,js,json,css}] +indent_size = 2 +indent_style = space +insert_final_newline = true +max_line_length = 120 + +[*.md] +trim_trailing_whitespace = false + +[*.{cmd,bat}] +end_of_line = crlf +insert_final_newline = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b9bb579 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,28 @@ +# +# 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. + +############################### +# Git Line Endings # +############################### + +# Set default behaviour to automatically normalize line endings. +* text=auto + +# Force the following filetypes to have unix eols, Windows can usually handle it well +*.* text eol=lf + +# Force batch scripts to always use CRLF line endings as they in some cases might not work correctly. +# Also if a repo is accessed in Windows via a file share from Linux, the scripts will work too +*.cmd text eol=crlf +*.bat text eol=crlf diff --git a/.github/workflows/assign_issue_to_project.yml b/.github/workflows/assign_issue_to_project.yml index 860ef1e..af00c64 100644 --- a/.github/workflows/assign_issue_to_project.yml +++ b/.github/workflows/assign_issue_to_project.yml @@ -1,5 +1,5 @@ # -# Copyright 2021 ABSA Group Limited +# 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. diff --git a/.github/workflows/dependent_items.yml b/.github/workflows/dependent_items.yml index 1d690f2..bbfa33c 100644 --- a/.github/workflows/dependent_items.yml +++ b/.github/workflows/dependent_items.yml @@ -1,5 +1,5 @@ # -# Copyright 2021 ABSA Group Limited +# 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. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5e5e97 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# +# 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. +# +# use glob syntax. +syntax: glob +*.ser +*.class +*~ +*.bak +#*.off +*.old + +# eclipse conf file +.settings +.classpath +.project +.manager +.scala_dependencies +.scalastyle + +# idea +.idea +*.iml + +# vs code +.vscode + +# building +target +build +null +tmp* +temp* +dist +test-output +build.log + +# other scm +.svn +.CVS +.hg* + +.bsp diff --git a/README.md b/README.md index 8ce7316..43de18f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,22 @@ -# balta +# Balta + Scala library to write Postgres DB code tests with + +Balta is a Scala library to help creating database tests, particularly testing Database functions. It is based on the +popular [ScalaTest](http://www.scalatest.org/) library and uses [PostgreSQL](https://www.postgresql.org/) as the +database engine. + +It's a natural complement to the use of [Fa-Db library](https://github.com/AbsaOSS/fa-db) in applications. + +## Expected test-case structure +1. _Transaction start_* +2. Insert needed data into tables +3. Call the function to be tested +4. Verify the return values of the function via the `verify` function provided +5. Verify the data un the tables +6. _Transaction rollback_* + + * 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 +in a known state before and after each test. diff --git a/balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala b/balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala new file mode 100644 index 0000000..830f2ff --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala @@ -0,0 +1,172 @@ +/* + * 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.balta + +import org.scalactic.source +import org.scalatest.Tag +import org.scalatest.funsuite.AnyFunSuite +import za.co.absa.balta.classes.DBFunction.DBFunctionWithPositionedParamsOnly +import za.co.absa.balta.classes.setter.{AllowedParamTypes, Params} +import za.co.absa.balta.classes.setter.Params.{NamedParams, OrderedParams} +import za.co.absa.balta.classes.{ConnectionInfo, DBConnection, DBFunction, DBTable, QueryResult} + +import java.sql.DriverManager +import java.time.OffsetDateTime +import java.util.Properties + +/** + * This is a base class for all DB tests. It inherits from AnyFunSuite and provides the following: + * * automatic creation and provision of a DB connection + * * an enhanced test function that automatically rolls back the transaction after the test is finished + * * easy access to DB tables and functions + * * the now() function that returns the current transaction time in the DB + */ +abstract class DBTestSuite extends AnyFunSuite { + + /* the DB connection is ``lazy`, so it actually can be created only when needed and therefore the credentials + overridden in the successor */ + protected lazy implicit val dbConnection: DBConnection = { + createConnection( + connectionInfo.dbUrl, + connectionInfo.username, + connectionInfo.password + ) + } + + /** + * This is the connection info for the DB. It can be overridden in the derived classes to provide specific credentials + */ + protected lazy val connectionInfo: ConnectionInfo = readConnectionInfoFromConfig + + /** + * This is an enhanced test function that automatically rolls back the transaction after the test is finished + * + * @param testName – the name of the test + * @param testTags – the optional list of tags for this test + * @param testFun – the test function + */ + override protected def test(testName: String, testTags: Tag*) + (testFun: => Any /* Assertion */) + (implicit pos: source.Position): Unit = { + val dbTestFun = { + try { + testFun + } + finally { + if (connectionInfo.persistData) { + dbConnection.connection.commit() + } else { + dbConnection.connection.rollback() + } + } + } + super.test(testName, testTags: _*)(dbTestFun) + } + + /** + * This is a helper function that allows to easily access a DB table + * @param tableName - the name of the table + * @return - the DBTable object + */ + protected def table(tableName: String): DBTable = { + DBTable(tableName) + } + + /** + * This is a helper function that allows to easily access a DB function + * @param functionName - the name of the function + * @return - the DBFunction object + */ + protected def function(functionName: String): DBFunctionWithPositionedParamsOnly = { + DBFunction(functionName) + } + + /** + * This is a helper function that allows to easily get the DB current time + * @param connection - the DB connection + * @return - the current transaction time + */ + protected def now()(implicit connection: DBConnection): OffsetDateTime = { + val preparedStatement = connection.connection.prepareStatement("SELECT now() AS now") + val prep = preparedStatement.executeQuery() + val result = new QueryResult(prep).next().getOffsetDateTime("now").get + prep.close() + result + } + + /** + * This is a helper function that allows to easily create parameter for table and function queries + * + * @param paramName - the name of the parameter + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + protected def add[T: AllowedParamTypes](paramName: String, value: T): NamedParams = { + Params.add(paramName, value) + } + + /** + * This is a helper function that allows to easily create parameter of value NULL for table and function queries + * + * @param paramName - the name of the parameter + * @return - a list parameters to be used in an SQL prepared statement + */ + protected def addNull(paramName: String): NamedParams = { + Params.addNull(paramName) + } + + /** + * This is a helper function that allows to easily create a positioned parameter for table and function queries + * + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + protected def add[T: AllowedParamTypes](value: T): OrderedParams = { + Params.add(value) + } + + /** + * This is a helper function that allows to easily create a positioned parameter of value NULL for table and function queries + * + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + protected def addNull[T: AllowedParamTypes](): OrderedParams = { + Params.addNull() + } + + // private functions + private def createConnection(url: String, username: String, password: String): DBConnection = { + val conn = DriverManager.getConnection(url, username, password) + conn.setAutoCommit(false) + new DBConnection(conn) + } + + private def readConnectionInfoFromConfig = { + val properties = new Properties() + properties.load(getClass.getResourceAsStream("/database.properties")) + + ConnectionInfo( + dbUrl = properties.getProperty("test.jdbc.url"), + username = properties.getProperty("test.jdbc.username"), + password = properties.getProperty("test.jdbc.password"), + persistData = properties.getProperty("test.persist.db", "false").toBoolean + ) + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala b/balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala new file mode 100644 index 0000000..9264424 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala @@ -0,0 +1,173 @@ +/* + * 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.balta.classes + +import za.co.absa.balta.classes.DBFunction.{DBFunctionWithNamedParamsToo, DBFunctionWithPositionedParamsOnly, ParamsMap} +import za.co.absa.balta.classes.setter.{AllowedParamTypes, SetterFnc} + +import scala.collection.immutable.ListMap + +/** + * A class that represents a database function call. It can be used to execute a function and verify the result. + * THere are two implementations of this class: + * - DBFunctionWithPositionedParamsOnly - the parameters are defined by their position solely + * - DBFunctionWithNamedParamsToo - there can be parameters defined by their position and and others defined by their + * name; note that the position defined parameters can be added only at the beginning of the parameter list + * + * @param functionName - the name of the function + * @param params - the list of parameters + */ +sealed abstract class DBFunction private(functionName: String, + params: ParamsMap) extends DBQuerySupport { + + private def sql(orderBy: String): String = { + val paramEntries = params.map{case(key, setterFnc) => + key match { + case Left(_) => setterFnc.sqlEntry + case Right(name) => s"$name := ${setterFnc.sqlEntry}" // TODO https://github.com/AbsaOSS/balta/issues/2 + } + } + val paramsLine = paramEntries.mkString(",") + s"SELECT * FROM $functionName($paramsLine) $orderBy" + } + + /** + * Executes the function and verifies the result via the verify function. + * + * @param verify - the function that verifies the result + * @param connection - the database connection + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def execute[R](verify: QueryResult => R /* Assertion */)(implicit connection: DBConnection): R = { + execute("")(verify) + } + + /** + * Executes the function and verifies the result via the verify function. + * + * @param orderBy - the clause how to order the function result + * @param verify - the function that verifies the result + * @param connection - the database connection + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def execute[R](orderBy: String)(verify: QueryResult => R /* Assertion */)(implicit connection: DBConnection): R = { + val orderByPart = if (orderBy.nonEmpty) {s"ORDER BY $orderBy"} else "" + runQuery(sql(orderByPart), params.values.toList)(verify) + } + + /** + * Sets a parameter for the function call. It actually creates a new instance of the DBFunction class with the new + * parameter. + * + * @param paramName - the name of the parameter to set + * @param value - the value of the parameter + * @return - a new instance of the DBFunction class with the new parameter + */ + def setParam[T: AllowedParamTypes](paramName: String, value: T): DBFunctionWithNamedParamsToo = { + val key = Right(paramName) // TODO normalization TODO https://github.com/AbsaOSS/balta/issues/1 + val fnc = SetterFnc.createSetterFnc(value) + DBFunctionWithNamedParamsToo(functionName, params + (key -> fnc)) + } + + /** + * Sets a parameter to NULL for the function call. It actually creates a new instance of the DBFunction class with + * the new parameter. + * + * @param paramName - the name of the parameter to set + * @return - a new instance of the DBFunction class with the new parameter + */ + def setParamNull(paramName: String): DBFunctionWithPositionedParamsOnly = { + val key = Right(paramName) // TODO normalization TODO https://github.com/AbsaOSS/balta/issues/1 + val fnc = SetterFnc.nullSetterFnc + DBFunctionWithPositionedParamsOnly(functionName, params + (key -> fnc)) + } + + /** + * Clears all parameters. It actually creates a new instance of the DBFunction class without any parameters. + * + * @return - a new instance of the DBFunction class without any parameters set + */ + def clear(): DBFunctionWithPositionedParamsOnly = { + DBFunctionWithPositionedParamsOnly(functionName) + } +} + + +object DBFunction { + + type ParamsMap = ListMap[Either[Int, String], SetterFnc] + + /** + * Creates a new instance of the DBFunction class with the given function name without any parameters set. + * + * @param functionName - the name of the function + * @return - a new instance of the DBFunction class + */ + def apply(functionName: String): DBFunctionWithPositionedParamsOnly = { + DBFunctionWithPositionedParamsOnly(functionName) + } + + /** + * Class that represents a database function call with parameters defined by their position only. It's the default + * class when creating a new instance of the DBFunction class without any parameters set. + * + * @param functionName - the name of the function + * @param params - the list of parameters + */ + sealed case class DBFunctionWithPositionedParamsOnly private(functionName: String, + params: ParamsMap = ListMap.empty + ) extends DBFunction(functionName, params) { + /** + * Sets a parameter for the function call. It actually creates a new instance of the DBFunction class with the new + * parameter. The new parameter is the last in the parameter list. + * + * @param value - the value of the parameter + * @return - a new instance of the DBFunction class with the new parameter + */ + def setParam[T: AllowedParamTypes](value: T): DBFunctionWithPositionedParamsOnly = { + val key = Left(params.size + 1) + val fnc = SetterFnc.createSetterFnc(value) + DBFunctionWithPositionedParamsOnly(functionName, params + (key -> fnc)) + } + + /** + * Sets a parameter to NULL for the function call. It actually creates a new instance of the DBFunction class with + * the new parameter. The new parameter is the last in the parameter list. + * + * @return - a new instance of the DBFunction class with the new parameter + */ + def setParamNull(): DBFunctionWithPositionedParamsOnly = { + val key = Left(params.size + 1) + val fnc = SetterFnc.nullSetterFnc + DBFunctionWithPositionedParamsOnly(functionName, params + (key -> fnc)) + } + + } + + /** + * Class that represents a database function call with parameters list where the paremeters can be defined by their + * position (at the beginning of the list) and by their name (for the rest of the list). + * + * @param functionName - the name of the function + * @param params - the list of parameters + */ + sealed case class DBFunctionWithNamedParamsToo private(functionName: String, + params: ParamsMap = ListMap.empty + ) extends DBFunction(functionName, params) +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala b/balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala new file mode 100644 index 0000000..2668b68 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala @@ -0,0 +1,43 @@ +/* + * 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.balta.classes + +import za.co.absa.balta.classes.setter.SetterFnc + +/** + * This is a based trait providing the ability to run an SQL query and verify the result via a provided function. + */ +trait DBQuerySupport { + + protected def runQuery[R](sql: String, setters: List[SetterFnc]) + (verify: QueryResult => R /* Assertion */) + (implicit connection: DBConnection): R = { + val preparedStatement = connection.connection.prepareStatement(sql) + + setters.foldLeft(1) { case (parameterIndex, fnc) => + fnc(preparedStatement, parameterIndex) + parameterIndex + 1 + } + + val result = preparedStatement.executeQuery() + try { + verify(new QueryResult(result)) + } finally { + result.close() + } + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala b/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala new file mode 100644 index 0000000..9e89b6d --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala @@ -0,0 +1,204 @@ +/* + * 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.balta.classes + +import za.co.absa.balta.classes.setter.{AllowedParamTypes, Params, SetterFnc} +import za.co.absa.balta.classes.setter.Params.NamedParams + +/** + * This class represents a database table. It allows to perform INSERT, SELECT and COUNT operations on the table easily. + * + * @param tableName The name of the table + */ +case class DBTable(tableName: String) extends DBQuerySupport{ + + /** + * Inserts a new row into the table. + * + * @param values - a map of column names and values to be inserted. + * @param connection - a database connection used for the INSERT operation. + * @return - the inserted row. + */ + def insert(values: Params)(implicit connection: DBConnection): QueryResultRow = { + val columns = values.keys.map {keys => + val keysString = keys.mkString(",") // TODO https://github.com/AbsaOSS/balta/issues/2 + s"($keysString)" + }.getOrElse("") + val paramStr = values.setters.map(_.sqlEntry).mkString(",") + val sql = s"INSERT INTO $tableName $columns VALUES($paramStr) RETURNING *;" + runQuery(sql, values.setters){_.next()} + } + + /** + * Returns a value of a field of a row selected by a key. + * + * @param keyName - the name of the key column + * @param keyValue - the value of the key column + * @param fieldName - the name of the field to be returned + * @param connection - a database connection used for the SELECT operation. + * @tparam K - the type of the key value + * @tparam T - the type of the returned field value + * @return - the value of the field, if the value is NULL, then `Some(None)` is returned; if no row is found, + * then `None` is returned. + */ + def fieldValue[K: AllowedParamTypes, T](keyName: String, keyValue: K, fieldName: String) + (implicit connection: DBConnection): Option[Option[T]] = { + where(Params.add(keyName, keyValue)){resultSet => + if (resultSet.hasNext) { + Some(resultSet.next().getAs[T](fieldName)) + } else { + None + } + } + } + + /** + * Selects the rows from the table based on the provided parameters and verifies the result via the verify function. + * @param params - the parameters used for the WHERE clause + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def where[R](params: NamedParams)(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(strToOption(paramsToWhereCondition(params)), None, params.setters)(verify) + } + + /** + * Selects the rows from the table based on the provided parameters and verifies the result via the verify function. + * @param params - the parameters used for the WHERE clause + * @param orderBy - the clause how to order the result + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def where[R](params: NamedParams, orderBy: String)(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(strToOption(paramsToWhereCondition(params)), strToOption(orderBy), params.setters)(verify) + } + + /** + * Selects the rows from the table based on the provided condition and verifies the result via the verify function. + * @param condition - the condition used for the WHERE clause + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def where[R](condition: String)(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(strToOption(condition), None)(verify) + } + + /** + * Selects the rows from the table based on the provided condition and verifies the result via the verify function. + * @param condition - the condition used for the WHERE clause + * @param orderBy - the clause how to order the result + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def where[R](condition: String, orderBy: String)(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(strToOption(condition), strToOption(orderBy))(verify) + } + + /** + * Returns all rows from the table and verifies the result via the verify function. + * + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def all[R]()(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(None, None)(verify) + } + + /** + * Returns all rows from the table and verifies the result via the verify function. + * + * @param orderBy - the clause how to order the result + * @param verify - the function that verifies the result + * @param connection - a database connection used for the SELECT operation. + * @tparam R - the type of the result that is returned by the verify function + * @return - the result of the verify function + */ + def all[R](orderBy: String)(verify: QueryResult => R)(implicit connection: DBConnection): R = { + composeSelectAndRun(None, strToOption(orderBy))(verify) + } + + /** + * Counts the rows in the table. + * @param connection - a database connection used for the SELECT operation. + * @return - the number of rows + */ + def count()(implicit connection: DBConnection): Long = { + composeCountAndRun(None) + } + + /** + * Counts the rows in the table based on the provided parameters. + * @param params - the parameters used for the WHERE clause + * @param connection - a database connection used for the SELECT operation. + * @return - the number of rows + */ + def count(params: NamedParams)(implicit connection: DBConnection): Long = { + composeCountAndRun(strToOption(paramsToWhereCondition(params)), params.setters) + } + + /** + * Counts the rows in the table based on the provided condition. + * @param condition - the condition used for the WHERE clause + * @param connection - a database connection used for the SELECT operation. + * @return - the number of rows + */ + def count(condition: String)(implicit connection: DBConnection): Long = { + composeCountAndRun(strToOption(condition)) + } + + private def composeSelectAndRun[R](whereCondition: Option[String], orderByExpr: Option[String], setters: List[SetterFnc] = List.empty) + (verify: QueryResult => R) + (implicit connection: DBConnection): R = { + val where = whereCondition.map("WHERE " + _).getOrElse("") + val orderBy = orderByExpr.map("ORDER BY " + _).getOrElse("") + val sql = s"SELECT * FROM $tableName $where $orderBy;" + runQuery(sql, setters)(verify) + } + + private def composeCountAndRun(whereCondition: Option[String], setters: List[SetterFnc] = List.empty) + (implicit connection: DBConnection): Long = { + val where = whereCondition.map("WHERE " + _).getOrElse("") + val sql = s"SELECT count(1) AS cnt FROM $tableName $where;" + runQuery(sql, setters) {resultSet => + resultSet.next().getLong("cnt").getOrElse(0) + } + } + + private def strToOption(str: String): Option[String] = { + if (str.isEmpty) { + None + } else { + Option(str) + } + } + + private def paramsToWhereCondition(params: NamedParams): String = { + params.pairs.foldLeft(List.empty[String]) {case (acc, (fieldName, setterFnc)) => + s"$fieldName = ${setterFnc.sqlEntry}" :: acc // TODO https://github.com/AbsaOSS/balta/issues/2 + }.mkString(" AND ") + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala b/balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala new file mode 100644 index 0000000..58a0f55 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala @@ -0,0 +1,47 @@ +/* + * 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.balta.classes + +import java.sql.ResultSet + +/** + * This is an iterator over the result of a query. + * + * @param resultSet - the JDBC result of a query + */ +class QueryResult(resultSet: ResultSet) extends Iterator[QueryResultRow] { + private [this] var resultSetHasNext: Option[Boolean] = Some(resultSet.next()) + + override def hasNext: Boolean = { + resultSetHasNext.getOrElse { + val result = resultSet.next() + resultSetHasNext = Some(result) + result + } + } + + override def next(): QueryResultRow = { + if (resultSetHasNext.isEmpty) { + resultSet.next() + new QueryResultRow(resultSet) + } else { + resultSetHasNext = None + new QueryResultRow(resultSet) + + } + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala b/balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala new file mode 100644 index 0000000..28969e5 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala @@ -0,0 +1,97 @@ +/* + * 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.balta.classes + +import org.postgresql.util.PGobject + +import java.sql.{Date, ResultSet, Time} +import java.time.{Instant, OffsetDateTime} +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 resultSet - the JDBC result of a query + */ +class QueryResultRow private[classes](val resultSet: ResultSet) extends AnyVal { + // this is not stable as resultSet mutates, but good enough for now + private def safe[T](fnc: => T): Option[T] = { + val result = fnc + if (resultSet.wasNull()) { + None + } else { + Some(result) + } + } + + def getBoolean(columnLabel: String): Option[Boolean] = safe(resultSet.getBoolean(columnLabel)) + + def getChar(columnLabel: String): Option[Char] = { + getString(columnLabel) match { + case Some(value) => + if (value.isEmpty) None else Some(value.charAt(0)) + case None => + None + } + } + + def getString(columnLabel: String): Option[String] = safe(resultSet.getString(columnLabel)) + + def getInt(columnLabel: String): Option[Int] = safe(resultSet.getInt(columnLabel)) + + def getLong(columnLabel: String): Option[Long] = safe(resultSet.getLong(columnLabel)) + + def getDouble(columnLabel: String): Option[Double] = safe(resultSet.getDouble(columnLabel)) + + def getFloat(columnLabel: String): Option[Float] = safe(resultSet.getFloat(columnLabel)) + + def getBigDecimal(columnLabel: String): Option[BigDecimal] = safe(resultSet.getBigDecimal(columnLabel)) + + def getUUID(columnLabel: String): Option[UUID] = Option(resultSet.getObject(columnLabel).asInstanceOf[UUID]) + + def getOffsetDateTime(columnLabel: String): Option[OffsetDateTime] = Option(resultSet.getObject(columnLabel, classOf[OffsetDateTime])) + + def getInstant(columnLabel: String): Option[Instant] = getOffsetDateTime(columnLabel).map(_.toInstant) + + def getTime(columnLabel: String): Option[Time] = safe(resultSet.getTime(columnLabel)) + + def getDate(columnLabel: String): Option[Date] = safe(resultSet.getDate(columnLabel)) + + def getJsonB(columnLabel: String): Option[JsonBString] = { + Option(resultSet.getObject(columnLabel).asInstanceOf[PGobject])map(pgo => JsonBString(pgo.toString)) + } + + def getArray[T](columnLabel: String): Option[Array[T]] = { + val array = resultSet.getArray(columnLabel) + if (resultSet.wasNull()) { + None + } else { + Option(array.getArray.asInstanceOf[Array[T]]) + } + } + + def getAs[T](columnLabel: String): Option[T] = { + val result = resultSet.getObject(columnLabel) + if (resultSet.wasNull()) { + None + } else { + val resultTyped = result.asInstanceOf[T] + Option(resultTyped) + } + } + +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/package.scala b/balta/src/main/scala/za/co/absa/balta/classes/package.scala new file mode 100644 index 0000000..e24fb09 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/package.scala @@ -0,0 +1,42 @@ +/* + * 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.balta + +import java.sql.Connection + +package object classes { + case class JsonBString(value: String) extends AnyVal + + class DBConnection(val connection: Connection) extends AnyVal + + /** + * This is a function that sets a parameter of a prepared statement. + * + * @param dbUrl - the JDBC URL of the database + * @param username - the username to use when connecting to the database + * @param password - the password to use when connecting to the database + * @param persistData - whether to persist the data to the database (usually false for tests, set to true for + * debugging purposes) + */ + case class ConnectionInfo( + dbUrl: String, + username: String, + password: String, + persistData: Boolean + ) + +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala b/balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala new file mode 100644 index 0000000..6db1171 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala @@ -0,0 +1,64 @@ +/* + * 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.balta.classes.setter + +import za.co.absa.balta.classes.JsonBString + +import java.time.{Instant, OffsetDateTime, LocalTime, LocalDate} +import java.util.UUID + +/** + * This is a function th + */ +sealed trait AllowedParamTypes[T] + +/** + * This object contains implicit instances of `AllowedParamTypes` for all supported types. + * It is used to restrict the types of parameters that can be passed to the `SetterFnc` function. + */ +object AllowedParamTypes { + // where new type is added remember to add an appropriate `SetterFnc` + implicit object BooleanParamType extends AllowedParamTypes[Boolean] + + implicit object IntParamType extends AllowedParamTypes[Int] + + implicit object LongParamType extends AllowedParamTypes[Long] + + implicit object DoubleParamType extends AllowedParamTypes[Double] + + implicit object FloatParamType extends AllowedParamTypes[Float] + + implicit object BigDecimalParamType extends AllowedParamTypes[BigDecimal] + + implicit object CharParamType extends AllowedParamTypes[Char] + + implicit object StringParamType extends AllowedParamTypes[String] + + implicit object UUIDParamType extends AllowedParamTypes[UUID] + + implicit object JsonBParamType extends AllowedParamTypes[JsonBString] + + implicit object InstantParamType extends AllowedParamTypes[Instant] + + implicit object OffsetDateTimeParamType extends AllowedParamTypes[OffsetDateTime] + + implicit object LocalTimeParamType extends AllowedParamTypes[LocalTime] + + implicit object LocalDateParamType extends AllowedParamTypes[LocalDate] + + implicit object CustomDBTypeParamType extends AllowedParamTypes[CustomDBType] +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala b/balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala new file mode 100644 index 0000000..78921c9 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala @@ -0,0 +1,25 @@ +/* + * 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.balta.classes.setter + +/** + * This is a case class representing a custom DB type. + * + * @param valueAsString - the value of the custom DB type as a string + * @param dbType - the DB type name + */ +case class CustomDBType(valueAsString: String, dbType: String) diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala b/balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala new file mode 100644 index 0000000..fc2effb --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala @@ -0,0 +1,157 @@ +/* + * 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.balta.classes.setter + +import scala.collection.immutable.ListMap + +/** + * This class represents a list of parameters for a prepared statement. It cannot be instantiated directly, only via + * provided factory methods. + * + * @param items - a list of parameter values, eventually with their names or positions + */ +sealed abstract class Params private(items: ListMap[String, SetterFnc]) { + def keys: Option[List[String]] + def setters: List[SetterFnc] = items.values.toList + + def apply(paramName: String): SetterFnc = { + items(paramName) + } + + def size: Int = items.size + +} +object Params { + + /** + * This is a factory method for creating a list of named parameters consisting of one parameter identified by its + * name. + * + * @param paramName - the name of the parameter + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def add[T: AllowedParamTypes](paramName: String, value: T): NamedParams = { + new NamedParams().add(paramName, value) + } + + /** + * This is a factory method for creating a list of named parameters consisting of one parameter identified by its + * name. The parameter value is NULL. + * + * @param paramName - the name of the parameter + * @return - a list parameters to be used in an SQL prepared statement + */ + def addNull(paramName: String): NamedParams = { + new NamedParams().addNull(paramName) + } + + /** + * This is a factory method for creating a list of ordered parameters consisting of one parameter identified by its + * position. + * + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def add[T: AllowedParamTypes](value: T): OrderedParams = { + new OrderedParams().add(value) + } + + /** + * This is a factory method for creating a list of ordered parameters consisting of one parameter identified by its + * position. The parameter value is NULL. + * + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def addNull[T: AllowedParamTypes](): OrderedParams = { + new OrderedParams().addNull() + } + + /** + * This is a class representing a list of named parameters. + * + * @param items - a map of parameter names and their values + */ + sealed class NamedParams private[setter](items: ListMap[String, SetterFnc] = ListMap.empty) extends Params(items) { + /** + * This method adds a new parameter to the list. It actually creates a new list with the new parameter added. + * + * @param paramName - the name of the parameter + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def add[T: AllowedParamTypes](paramName: String, value: T): NamedParams = { + val setter = SetterFnc.createSetterFnc(value) + new NamedParams(items + (paramName -> setter)) // TODO https://github.com/AbsaOSS/balta/issues/1 + } + + /** + * This method adds a new parameter to the list. It actually creates a new list with the new parameter added. + * The parameter value is NULL. + * + * @param paramName - the name of the parameter + * @return - a list parameters to be used in an SQL prepared statement + */ + def addNull(paramName: String): NamedParams = { + val setter = SetterFnc.nullSetterFnc + new NamedParams(items + (paramName -> setter)) // TODO https://github.com/AbsaOSS/balta/issues/1 + } + + def pairs: List[(String, SetterFnc)] = items.toList + + override def keys: Option[List[String]] = Some(items.keys.toList) + } + + /** + * This is a class representing a list of parameters represented by their position + * + * @param items - a list of parameters identified by their positions + */ + sealed class OrderedParams private[setter](items: ListMap[String, SetterFnc] = ListMap.empty) extends Params(items) { + /** + * This method adds a new parameter to the end of the list. It actually creates a new list with the new parameter added. + * + * @param value - the value of the parameter + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def add[T: AllowedParamTypes](value: T): OrderedParams = { + val key = items.size.toString + val setter = SetterFnc.createSetterFnc(value) + new OrderedParams(items + (key -> setter)) + } + + /** + * This method adds a new parameter to the end of the list. It actually creates a new list with the new parameter added. + * The parameter value is NULL. + * + * @tparam T - the type of the parameter value + * @return - a list parameters to be used in an SQL prepared statement + */ + def addNull[T: AllowedParamTypes](): OrderedParams = { + val key = items.size.toString + val setter = SetterFnc.nullSetterFnc + new OrderedParams(items + (key -> setter)) + } + + override val keys: Option[List[String]] = None + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala b/balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala new file mode 100644 index 0000000..e8e3b9f --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala @@ -0,0 +1,94 @@ +/* + * 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.balta.classes.setter + +import za.co.absa.balta.classes.JsonBString + +import java.sql.{Date, PreparedStatement, Time, Timestamp, Types => SqlTypes} +import java.util.UUID +import org.postgresql.util.PGobject + +import java.time.{Instant, LocalDate, LocalTime, OffsetDateTime, ZoneId, ZoneOffset} + +/** + * This is a trait representing a function that sets a value in a prepared statement. + */ +abstract class SetterFnc extends ((PreparedStatement, Int) => Unit) { + def sqlEntry: String = "?" +} + +object SetterFnc { + + /** + * This method creates a `SetterFnc` for a given value. + * + * @param value - the value to be set + * @tparam T - the type of the value + * @return - a function that sets the given value in a prepared statement + */ + def createSetterFnc[T: AllowedParamTypes](value: T): SetterFnc = { + value match { + case b: Boolean => simple((prep: PreparedStatement, position: Int) => {prep.setBoolean(position, b)}) + case i: Int => simple((prep: PreparedStatement, position: Int) => {prep.setInt(position, i)}) + case l: Long => simple((prep: PreparedStatement, position: Int) => {prep.setLong(position, l)}) + case d: Double => simple((prep: PreparedStatement, position: Int) => {prep.setDouble(position, d)}) + case f: Float => simple((prep: PreparedStatement, position: Int) => {prep.setFloat(position, f)}) + case bd: BigDecimal => simple((prep: PreparedStatement, position: Int) => {prep.setBigDecimal(position, bd.bigDecimal)}) + case ch: Char => simple((prep: PreparedStatement, position: Int) => {prep.setString(position, ch.toString)}) + case s: String => simple((prep: PreparedStatement, position: Int) => {prep.setString(position, s)}) + case u: UUID => new UuidSetterFnc(u) + case js: JsonBString => new JsonBSetterFnc(js) + case i: Instant => simple((prep: PreparedStatement, position: Int) => {prep.setObject(position, OffsetDateTime.ofInstant(i, ZoneOffset.UTC))}) + case ts: OffsetDateTime => simple((prep: PreparedStatement, position: Int) => {prep.setObject(position, ts)}) + case lt: LocalTime => simple((prep: PreparedStatement, position: Int) => {prep.setTime(position, Time.valueOf(lt))}) + case ld: LocalDate => simple((prep: PreparedStatement, position: Int) => {prep.setDate(position, Date.valueOf(ld))}) + case CustomDBType(value, dbType) => new CustomDBTypeSetterFnc(value, dbType) + } + } + + val nullSetterFnc: SetterFnc = simple((prep: PreparedStatement, position: Int) => prep.setNull(position, SqlTypes.NULL)) + + private [this] def simple(body: (PreparedStatement, Int) => Unit): SetterFnc = { + new SetterFnc { + override def apply(prep: PreparedStatement, position: Int): Unit = body(prep, position) + } + } + + private class UuidSetterFnc(value: UUID) extends SetterFnc { + def apply(prep: PreparedStatement, position: Int): Unit = { + prep.setObject(position, value) + } + } + + private class JsonBSetterFnc(value: JsonBString) extends SetterFnc { + private val jsonObject = new PGobject() + jsonObject.setType("jsonb") + jsonObject.setValue(value.value) + + def apply(prep: PreparedStatement, position: Int): Unit = { + prep.setObject(position, jsonObject) + } + } + + private class CustomDBTypeSetterFnc(value: String, dbType: String) extends SetterFnc { + def apply(prep: PreparedStatement, position: Int): Unit = { + prep.setString(position, value) + } + + override def sqlEntry: String = s"?::$dbType" + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/implicits/package.scala b/balta/src/main/scala/za/co/absa/balta/implicits/package.scala new file mode 100644 index 0000000..3c13977 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/balta/implicits/package.scala @@ -0,0 +1,31 @@ +/* + * 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.balta + +import za.co.absa.balta.classes.DBConnection + +import java.sql.Connection +import scala.language.implicitConversions + +package object implicits { + + /** + * This implicit conversion allows to use a DBConnection at any place where as a JDBC Connection is required. + */ + implicit def dbConnectionToJdbcConnection(in: DBConnection): Connection = in.connection + +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..57703ff --- /dev/null +++ b/build.sbt @@ -0,0 +1,35 @@ +/* + * 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. + */ + +import Dependencies._ + +ThisBuild / organization := "za.co.absa.balta" + +lazy val scala211 = "2.11.12" +lazy val scala212 = "2.12.18" +lazy val scala213 = "2.13.11" + +lazy val supportedScalaVersions: Seq[String] = Seq(scala211, scala212 , scala213) + +ThisBuild / scalaVersion := scala212 + +lazy val balta = (project in file("balta")) + .settings( + name := "balta", + crossScalaVersions := supportedScalaVersions, + + libraryDependencies ++= libDependencies + ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..26eaa2e --- /dev/null +++ b/project/Dependencies.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. + */ + +import sbt._ + +object Dependencies { + + object Versions { + val scalatest = "3.2.15" + + val postgresql = "42.6.0" + } + + def libDependencies: Seq[ModuleID] = { + lazy val scalaTest = "org.scalatest" %% "scalatest" % Versions.scalatest + lazy val postgresql = "org.postgresql" % "postgresql" % Versions.postgresql + Seq( + scalaTest, + postgresql, + ) + } +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..6aeb949 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.2