From ae4433c7139ef36bbf88bf92cba625ff332f6e22 Mon Sep 17 00:00:00 2001 From: Ladislav Sulak Date: Mon, 11 Dec 2023 18:30:34 +0100 Subject: [PATCH 1/7] Fixing The Release (sonatypeProfileName was slightly incorrect) (#17) * adding missed versionScheme and chanigng organization to be za.co.absa; this will be used for the sonatypeProfileName --- build.sbt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 4b49c30..d1edc01 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ import Dependencies._ -ThisBuild / organization := "za.co.absa.balta" +ThisBuild / organization := "za.co.absa" lazy val scala211 = "2.11.12" lazy val scala212 = "2.12.18" @@ -26,6 +26,8 @@ lazy val supportedScalaVersions: Seq[String] = Seq(scala211, scala212 , scala213 ThisBuild / scalaVersion := scala212 +ThisBuild / versionScheme := Some("early-semver") + lazy val balta = (project in file("balta")) .settings( name := "balta", From 4acf802925859c76bc74b938e7e92deb007c4bfb Mon Sep 17 00:00:00 2001 From: jakipatryk Date: Mon, 10 Jun 2024 17:41:42 +0200 Subject: [PATCH 2/7] #24 Fix DBTable NamedParams to SQL where condition generation (#25) --- balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 9e89b6d..0f142b1 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala +++ b/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala @@ -197,7 +197,7 @@ case class DBTable(tableName: String) extends DBQuerySupport{ } private def paramsToWhereCondition(params: NamedParams): String = { - params.pairs.foldLeft(List.empty[String]) {case (acc, (fieldName, setterFnc)) => + params.pairs.foldRight(List.empty[String]) {case ((fieldName, setterFnc), acc) => s"$fieldName = ${setterFnc.sqlEntry}" :: acc // TODO https://github.com/AbsaOSS/balta/issues/2 }.mkString(" AND ") } From 797164689facedbbf822c38b6b6aa69ad94f3097 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Thu, 1 Aug 2024 14:44:11 +0200 Subject: [PATCH 3/7] #26: Move code into sub package db (#28) * Fixed badge link in README.md * Update README.md --------- Co-authored-by: Ladislav Sulak Co-authored-by: miroslavpojer --- README.md | 15 +++++-- .../za/co/absa/balta/classes/package.scala | 42 ------------------- .../co/absa/{ => db}/balta/DBTestSuite.scala | 26 ++++-------- .../absa/db/balta/classes/DBConnection.scala | 31 ++++++++++++++ .../{ => db}/balta/classes/DBFunction.scala | 6 +-- .../balta/classes/DBQuerySupport.scala | 4 +- .../absa/{ => db}/balta/classes/DBTable.scala | 6 +-- .../{ => db}/balta/classes/QueryResult.scala | 2 +- .../balta/classes/QueryResultRow.scala | 3 +- .../classes/setter/AllowedParamTypes.scala | 6 +-- .../balta/classes/setter/CustomDBType.scala | 2 +- .../balta/classes/setter/Params.scala | 2 +- .../balta/classes/setter/SetterFnc.scala | 5 +-- .../balta/classes/simple/ConnectionInfo.scala | 33 +++++++++++++++ .../db/balta/classes/simple/JsonBString.scala | 19 +++++++++ .../{ => db}/balta/implicits/package.scala | 4 +- build.sbt | 2 - publish.sbt | 2 + 18 files changed, 124 insertions(+), 86 deletions(-) delete mode 100644 balta/src/main/scala/za/co/absa/balta/classes/package.scala rename balta/src/main/scala/za/co/absa/{ => db}/balta/DBTestSuite.scala (87%) create mode 100644 balta/src/main/scala/za/co/absa/db/balta/classes/DBConnection.scala rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/DBFunction.scala (97%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/DBQuerySupport.scala (93%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/DBTable.scala (98%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/QueryResult.scala (97%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/QueryResultRow.scala (97%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/setter/AllowedParamTypes.scala (92%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/setter/CustomDBType.scala (95%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/setter/Params.scala (99%) rename balta/src/main/scala/za/co/absa/{ => db}/balta/classes/setter/SetterFnc.scala (97%) create mode 100644 balta/src/main/scala/za/co/absa/db/balta/classes/simple/ConnectionInfo.scala create mode 100644 balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala rename balta/src/main/scala/za/co/absa/{ => db}/balta/implicits/package.scala (92%) diff --git a/README.md b/README.md index 30c9af6..200f97a 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,19 @@ # Balta -Scala library to write Postgres DB code tests with +Balta makes testing DB entities from Scala easier. It's primarily focused on testing the behavior of [Postgres functions](https://www.postgresql.org/docs/current/xfunc.html). + +--- + +### Build Status + +[![Build](https://github.com/AbsaOSS/balta/workflows/Build/badge.svg)](https://github.com/AbsaOSS/balta/actions) + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/za.co.absa.db/balta_2.12/badge.svg)](https://search.maven.org/search?q=g:za.co.absa.db.balta) + +--- 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. +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. 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 deleted file mode 100644 index e24fb09..0000000 --- a/balta/src/main/scala/za/co/absa/balta/classes/package.scala +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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/DBTestSuite.scala b/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala similarity index 87% rename from balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala rename to balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala index 830f2ff..15e7eb8 100644 --- a/balta/src/main/scala/za/co/absa/balta/DBTestSuite.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala @@ -14,17 +14,17 @@ * limitations under the License. */ -package za.co.absa.balta +package za.co.absa.db.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 za.co.absa.db.balta.classes.DBFunction.DBFunctionWithPositionedParamsOnly +import classes.setter.{AllowedParamTypes, Params} +import za.co.absa.db.balta.classes.setter.Params.{NamedParams, OrderedParams} +import classes.{DBConnection, DBFunction, DBTable, QueryResult} +import za.co.absa.db.balta.classes.simple.ConnectionInfo -import java.sql.DriverManager import java.time.OffsetDateTime import java.util.Properties @@ -39,13 +39,7 @@ 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 - ) - } + protected lazy implicit val dbConnection: DBConnection = DBConnection(connectionInfo) /** * This is the connection info for the DB. It can be overridden in the derived classes to provide specific credentials @@ -152,12 +146,6 @@ abstract class DBTestSuite extends AnyFunSuite { } // 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")) diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/DBConnection.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/DBConnection.scala new file mode 100644 index 0000000..e3a1c87 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/DBConnection.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.db.balta.classes + +import za.co.absa.db.balta.classes.simple.ConnectionInfo + +import java.sql.{Connection, DriverManager} + +class DBConnection(val connection: Connection) extends AnyVal + +object DBConnection { + def apply(connectionInfo: =>ConnectionInfo): DBConnection = { + val connection = DriverManager.getConnection(connectionInfo.dbUrl, connectionInfo.username, connectionInfo.password) + connection.setAutoCommit(false) + new DBConnection(connection) + } +} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala similarity index 97% rename from balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala index 9264424..8f39e50 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/DBFunction.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/DBFunction.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package za.co.absa.balta.classes +package za.co.absa.db.balta.classes -import za.co.absa.balta.classes.DBFunction.{DBFunctionWithNamedParamsToo, DBFunctionWithPositionedParamsOnly, ParamsMap} -import za.co.absa.balta.classes.setter.{AllowedParamTypes, SetterFnc} +import DBFunction.{DBFunctionWithNamedParamsToo, DBFunctionWithPositionedParamsOnly, ParamsMap} +import za.co.absa.db.balta.classes.setter.{AllowedParamTypes, SetterFnc} import scala.collection.immutable.ListMap diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala similarity index 93% rename from balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala index 2668b68..ac3e1f1 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/DBQuerySupport.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/DBQuerySupport.scala @@ -14,9 +14,9 @@ * limitations under the License. */ -package za.co.absa.balta.classes +package za.co.absa.db.balta.classes -import za.co.absa.balta.classes.setter.SetterFnc +import za.co.absa.db.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. diff --git a/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala similarity index 98% rename from balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala index 0f142b1..9281125 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/DBTable.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala @@ -14,10 +14,10 @@ * limitations under the License. */ -package za.co.absa.balta.classes +package za.co.absa.db.balta.classes -import za.co.absa.balta.classes.setter.{AllowedParamTypes, Params, SetterFnc} -import za.co.absa.balta.classes.setter.Params.NamedParams +import za.co.absa.db.balta.classes.setter.{AllowedParamTypes, Params, SetterFnc} +import za.co.absa.db.balta.classes.setter.Params.NamedParams /** * This class represents a database table. It allows to perform INSERT, SELECT and COUNT operations on the table easily. diff --git a/balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala similarity index 97% rename from balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala index 58a0f55..a429a6a 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/QueryResult.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package za.co.absa.balta.classes +package za.co.absa.db.balta.classes import java.sql.ResultSet diff --git a/balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala similarity index 97% rename from balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala index 28969e5..ab474a4 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/QueryResultRow.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResultRow.scala @@ -14,9 +14,10 @@ * limitations under the License. */ -package za.co.absa.balta.classes +package za.co.absa.db.balta.classes import org.postgresql.util.PGobject +import za.co.absa.db.balta.classes.simple.JsonBString import java.sql.{Date, ResultSet, Time} import java.time.{Instant, OffsetDateTime} diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/AllowedParamTypes.scala similarity index 92% rename from balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/setter/AllowedParamTypes.scala index 6db1171..7383c40 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/setter/AllowedParamTypes.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/AllowedParamTypes.scala @@ -14,11 +14,11 @@ * limitations under the License. */ -package za.co.absa.balta.classes.setter +package za.co.absa.db.balta.classes.setter -import za.co.absa.balta.classes.JsonBString +import za.co.absa.db.balta.classes.simple.JsonBString -import java.time.{Instant, OffsetDateTime, LocalTime, LocalDate} +import java.time.{Instant, LocalDate, LocalTime, OffsetDateTime} import java.util.UUID /** diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/CustomDBType.scala similarity index 95% rename from balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/setter/CustomDBType.scala index 78921c9..2c23ef6 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/setter/CustomDBType.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/CustomDBType.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package za.co.absa.balta.classes.setter +package za.co.absa.db.balta.classes.setter /** * This is a case class representing a custom DB type. diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/Params.scala similarity index 99% rename from balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/setter/Params.scala index fc2effb..e428b40 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/setter/Params.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/Params.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package za.co.absa.balta.classes.setter +package za.co.absa.db.balta.classes.setter import scala.collection.immutable.ListMap diff --git a/balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/SetterFnc.scala similarity index 97% rename from balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala rename to balta/src/main/scala/za/co/absa/db/balta/classes/setter/SetterFnc.scala index e8e3b9f..02ba649 100644 --- a/balta/src/main/scala/za/co/absa/balta/classes/setter/SetterFnc.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/setter/SetterFnc.scala @@ -14,13 +14,12 @@ * limitations under the License. */ -package za.co.absa.balta.classes.setter - -import za.co.absa.balta.classes.JsonBString +package za.co.absa.db.balta.classes.setter import java.sql.{Date, PreparedStatement, Time, Timestamp, Types => SqlTypes} import java.util.UUID import org.postgresql.util.PGobject +import za.co.absa.db.balta.classes.simple.JsonBString import java.time.{Instant, LocalDate, LocalTime, OffsetDateTime, ZoneId, ZoneOffset} diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/simple/ConnectionInfo.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/ConnectionInfo.scala new file mode 100644 index 0000000..c2ff9b6 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/ConnectionInfo.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.classes.simple + +/** + * 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/db/balta/classes/simple/JsonBString.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala new file mode 100644 index 0000000..13a8b4a --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala @@ -0,0 +1,19 @@ +/* + * 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.classes.simple + +case class JsonBString(value: String) extends AnyVal diff --git a/balta/src/main/scala/za/co/absa/balta/implicits/package.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/package.scala similarity index 92% rename from balta/src/main/scala/za/co/absa/balta/implicits/package.scala rename to balta/src/main/scala/za/co/absa/db/balta/implicits/package.scala index 3c13977..c3e9388 100644 --- a/balta/src/main/scala/za/co/absa/balta/implicits/package.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/package.scala @@ -14,9 +14,9 @@ * limitations under the License. */ -package za.co.absa.balta +package za.co.absa.db.balta -import za.co.absa.balta.classes.DBConnection +import za.co.absa.db.balta.classes.DBConnection import java.sql.Connection import scala.language.implicitConversions diff --git a/build.sbt b/build.sbt index d1edc01..52093c3 100644 --- a/build.sbt +++ b/build.sbt @@ -16,8 +16,6 @@ import Dependencies._ -ThisBuild / organization := "za.co.absa" - lazy val scala211 = "2.11.12" lazy val scala212 = "2.12.18" lazy val scala213 = "2.13.11" diff --git a/publish.sbt b/publish.sbt index 93d5266..d7d9e55 100644 --- a/publish.sbt +++ b/publish.sbt @@ -49,6 +49,8 @@ ThisBuild / developers := List( ) ) +ThisBuild / organization := "za.co.absa.db" + ThisBuild / organizationName := "ABSA Group Limited" ThisBuild / organizationHomepage := Some(url("https://www.absa.africa")) From 8b2b77aa3cec582508ed98a1634fc49bf502788f Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:34:59 +0200 Subject: [PATCH 4/7] #20: count when condition is provided is misleading (#30) --- .../za/co/absa/db/balta/classes/DBTable.scala | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala index 9281125..7ae8eb2 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/DBTable.scala @@ -156,6 +156,7 @@ case class DBTable(tableName: String) extends DBQuerySupport{ * @param connection - a database connection used for the SELECT operation. * @return - the number of rows */ + @deprecated("Use countOnCondition instead", "0.2.0") def count(params: NamedParams)(implicit connection: DBConnection): Long = { composeCountAndRun(strToOption(paramsToWhereCondition(params)), params.setters) } @@ -166,10 +167,31 @@ case class DBTable(tableName: String) extends DBQuerySupport{ * @param connection - a database connection used for the SELECT operation. * @return - the number of rows */ + @deprecated("Use countOnCondition instead", "0.2.0") def count(condition: String)(implicit connection: DBConnection): Long = { composeCountAndRun(strToOption(condition)) } + /** + * Counts the rows in the table based on the provided parameters that form a condition. + * @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 countOnCondition(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 countOnCondition(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 = { From dad52d88016952531f9c3bd42611f5b08f2afe83 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:28:12 +0200 Subject: [PATCH 5/7] #4: QueryResultRow does not instantiate the row data (#29) * `QueryResultRow` is now instantiated, so can be used even after connection close * `JsonBString` deprecated, replaced by `SimpleJsonString` * setup for separation of unit and integration tests * integration tests database setup * github build action enhanced to support integration tests --------- Co-authored-by: Ladislav Sulak --- .github/workflows/build.yml | 31 ++- .github/workflows/test_filenames_check.yml | 41 ++++ .sbtrc | 27 +++ README.md | 17 ++ .../za/co/absa/db/balta/DBTestSuite.scala | 2 +- .../absa/db/balta/classes/QueryResult.scala | 36 ++- .../db/balta/classes/QueryResultRow.scala | 159 +++++++++---- .../db/balta/classes/simple/JsonBString.scala | 1 + .../classes/simple/SimpleJsonString.scala | 25 ++ .../co/absa/db/balta/implicits/Postgres.scala | 41 ++++ balta/src/test/resources/database.properties | 8 + .../src/test/resources/db/postgres/01_db.ddl | 6 + .../test/resources/db/postgres/02_users.ddl | 9 + .../db/postgres/03_schema_testing.ddl | 2 + .../db/postgres/04_testing.base_types.ddl | 22 ++ .../postgres/05_testing._base_types_data.sql | 13 ++ .../db/postgres/06_testing.pg_types.ddl | 11 + .../db/postgres/07_testing_pg_types_data.sql | 15 ++ .../QueryResultRowIntegrationTests.scala | 221 ++++++++++++++++++ .../PostgresRowIntegrationTests.scala | 78 +++++++ .../testing/classes/DBTestingConnection.scala | 40 ++++ build.sbt | 2 + 22 files changed, 749 insertions(+), 58 deletions(-) create mode 100644 .github/workflows/test_filenames_check.yml create mode 100644 .sbtrc create mode 100644 balta/src/main/scala/za/co/absa/db/balta/classes/simple/SimpleJsonString.scala create mode 100644 balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala create mode 100644 balta/src/test/resources/database.properties create mode 100644 balta/src/test/resources/db/postgres/01_db.ddl create mode 100644 balta/src/test/resources/db/postgres/02_users.ddl create mode 100644 balta/src/test/resources/db/postgres/03_schema_testing.ddl create mode 100644 balta/src/test/resources/db/postgres/04_testing.base_types.ddl create mode 100644 balta/src/test/resources/db/postgres/05_testing._base_types_data.sql create mode 100644 balta/src/test/resources/db/postgres/06_testing.pg_types.ddl create mode 100644 balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql create mode 100644 balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala create mode 100644 balta/src/test/scala/za/co/absa/db/balta/testing/classes/DBTestingConnection.scala diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 256a7e7..f2982b7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,13 +33,42 @@ jobs: name: Scala ${{matrix.scala}} + services: + postgres: + image: postgres:15 + env: + POSTGRES_PASSWORD: postgres + POSTGRES_DB: mag_db + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + steps: - name: Checkout code uses: actions/checkout@v4 + - uses: coursier/cache-action@v5 + - name: Setup Scala uses: olafurpg/setup-scala@v10 with: java-version: "adopt@1.8" - - name: Build and run tests + + - name: Build and run unit tests run: sbt ++${{matrix.scala}} test doc + + - name: Setup database + run: | + psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/02_users.ddl + psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/03_schema_testing.ddl + psql postgresql://postgres:postgres@localhost:5432/mag_db -f balta/src/test/resources/db/postgres/04_testing.base_types.ddl + 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 + + - name: Build and run integration tests + run: sbt ++${{matrix.scala}} testIT diff --git a/.github/workflows/test_filenames_check.yml b/.github/workflows/test_filenames_check.yml new file mode 100644 index 0000000..318662b --- /dev/null +++ b/.github/workflows/test_filenames_check.yml @@ -0,0 +1,41 @@ +# +# 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. +# + +name: Test Filenames Check + +on: + pull_request: + branches: [ master ] + types: [ opened, synchronize, reopened ] + +jobs: + test_filenames_check: + name: Test Filenames Check + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Filename Inspector + id: scan-test-files + uses: AbsaOSS/filename-inspector@v0.1.0 + with: + name-patterns: '*UnitTests.*,*IntegrationTests.*' + paths: '**/src/test/scala/**' + report-format: 'console' + excludes: 'balta/src/test/scala/za/co/absa/db/balta/testing/*' + verbose-logging: 'false' + fail-on-violation: 'true' diff --git a/.sbtrc b/.sbtrc new file mode 100644 index 0000000..5f69dcb --- /dev/null +++ b/.sbtrc @@ -0,0 +1,27 @@ +# +# 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. +# + +# Aliases in this file expected usage of test file naming conventions: +# - "UnitTests" suffix for test files and Suites which define unit tests +# - "IntegrationTests" suffix for test files and Suites which define integration tests + +# CPS QA types aliases +# * Unit tests +alias test=; testOnly *UnitTests + +# * Integration tests +alias testIT=; testOnly *IntegrationTests + diff --git a/README.md b/README.md index 200f97a..e7ec7a4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,23 @@ It's a natural complement to the use of [Fa-Db library](https://github.com/AbsaO 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. +## How to 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. +* 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 + ## How to Release Please see [this file](RELEASE.md) for more details. + +## Known Issues + +### 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 diff --git a/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala b/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala index 15e7eb8..ba310ed 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/DBTestSuite.scala @@ -146,7 +146,7 @@ abstract class DBTestSuite extends AnyFunSuite { } // private functions - private def readConnectionInfoFromConfig = { + private def readConnectionInfoFromConfig: ConnectionInfo = { val properties = new Properties() properties.load(getClass.getResourceAsStream("/database.properties")) diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala index a429a6a..aa3a987 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/QueryResult.scala @@ -16,7 +16,9 @@ package za.co.absa.db.balta.classes -import java.sql.ResultSet +import za.co.absa.db.balta.classes.QueryResultRow.{Extractors, FieldNames} + +import java.sql.{ResultSet, ResultSetMetaData, SQLException} /** * This is an iterator over the result of a query. @@ -24,24 +26,34 @@ import java.sql.ResultSet * @param resultSet - the JDBC result of a query */ class QueryResult(resultSet: ResultSet) extends Iterator[QueryResultRow] { - private [this] var resultSetHasNext: Option[Boolean] = Some(resultSet.next()) + val resultSetMetaData: ResultSetMetaData = resultSet.getMetaData + val columnCount: Int = resultSetMetaData.getColumnCount + + private [this] var nextRow: Option[QueryResultRow] = None + + private [this] implicit val fieldNames: FieldNames = QueryResultRow.fieldNamesFromMetadata(resultSetMetaData) + private [this] implicit val extractors: Extractors = QueryResultRow.createExtractors(resultSetMetaData) override def hasNext: Boolean = { - resultSetHasNext.getOrElse { - val result = resultSet.next() - resultSetHasNext = Some(result) - result + if (nextRow.isEmpty) { + try { + if (resultSet.next()) { + nextRow = Some(QueryResultRow(resultSet)) + } + } catch { + case _: SQLException => // Do nothing + } } + nextRow.nonEmpty } override def next(): QueryResultRow = { - if (resultSetHasNext.isEmpty) { - resultSet.next() - new QueryResultRow(resultSet) + if (hasNext) { + val row = nextRow.get + nextRow = None + row } else { - resultSetHasNext = None - new QueryResultRow(resultSet) - + throw new NoSuchElementException("No more rows in the result set") } } } 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 ab474a4..337aceb 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 @@ -16,83 +16,154 @@ package za.co.absa.db.balta.classes -import org.postgresql.util.PGobject -import za.co.absa.db.balta.classes.simple.JsonBString +import QueryResultRow._ -import java.sql.{Date, ResultSet, Time} -import java.time.{Instant, OffsetDateTime} +import java.sql +import java.sql.{Date, ResultSet, ResultSetMetaData, Time, Types} +import java.time.{Instant, LocalDateTime, OffsetDateTime, OffsetTime} 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 + * @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 */ -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) - } - } +class QueryResultRow private[classes](val rowNumber: Int, + private val fields: Vector[Option[Object]], + private val columnNames: FieldNames) { + + def columnCount: Int = fields.length + def columnNumber(columnLabel: String): Int = columnNames(columnLabel.toLowerCase) + + def apply(column: Int): Option[Object] = fields(column - 1) + def apply(columnLabel: String): Option[Object] = apply(columnNumber(columnLabel)) - def getBoolean(columnLabel: String): Option[Boolean] = safe(resultSet.getBoolean(columnLabel)) + def getAs[T](column: Int, transformer: TransformerFnc[T]): Option[T] = apply(column).map(transformer) + def getAs[T](column: Int): Option[T] = apply(column)map(_.asInstanceOf[T]) - def getChar(columnLabel: String): Option[Char] = { - getString(columnLabel) match { + def getAs[T](columnLabel: String, transformer: TransformerFnc[T]): Option[T] = getAs(columnNumber(columnLabel), transformer) + def getAs[T](columnLabel: String): Option[T] = apply(columnNumber(columnLabel)).map(_.asInstanceOf[T]) + + def getBoolean(column: Int): Option[Boolean] = getAs(column: Int, {item: Object => item.asInstanceOf[Boolean]}) + def getBoolean(columnLabel: String): Option[Boolean] = getBoolean(columnNumber(columnLabel)) + + def getChar(column: Int): Option[Char] = { + getString(column) match { case Some(value) => if (value.isEmpty) None else Some(value.charAt(0)) case None => None } } + def getChar(columnLabel: String): Option[Char] = getChar(columnNumber(columnLabel)) - def getString(columnLabel: String): Option[String] = safe(resultSet.getString(columnLabel)) - def getInt(columnLabel: String): Option[Int] = safe(resultSet.getInt(columnLabel)) + def getString(column: Int): Option[String] = getAs(column: Int, {item: Object => item.asInstanceOf[String]}) + def getString(columnLabel: String): Option[String] = getString(columnNumber(columnLabel)) - def getLong(columnLabel: String): Option[Long] = safe(resultSet.getLong(columnLabel)) + def getInt(column: Int): Option[Int] = getAs(column: Int, {item: Object => item.asInstanceOf[Int]}) + def getInt(columnLabel: String): Option[Int] = getInt(columnNumber(columnLabel)) - def getDouble(columnLabel: String): Option[Double] = safe(resultSet.getDouble(columnLabel)) + def getLong(column: Int): Option[Long] = getAs(column: Int, {item: Object => item.asInstanceOf[Long]}) + def getLong(columnLabel: String): Option[Long] = getLong(columnNumber(columnLabel)) - def getFloat(columnLabel: String): Option[Float] = safe(resultSet.getFloat(columnLabel)) + def getDouble(column: Int): Option[Double] = getAs(column: Int, {item: Object => item.asInstanceOf[Double]}) + def getDouble(columnLabel: String): Option[Double] = getDouble(columnNumber(columnLabel)) - def getBigDecimal(columnLabel: String): Option[BigDecimal] = safe(resultSet.getBigDecimal(columnLabel)) + def getFloat(column: Int): Option[Float] = getAs(column: Int, {item: Object => item.asInstanceOf[Float]}) + def getFloat(columnLabel: String): Option[Float] = getFloat(columnNumber(columnLabel)) - def getUUID(columnLabel: String): Option[UUID] = Option(resultSet.getObject(columnLabel).asInstanceOf[UUID]) + def getBigDecimal(column: Int): Option[BigDecimal] = + getAs(column: Int, {item: Object => item.asInstanceOf[java.math.BigDecimal]}) + .map(scala.math.BigDecimal(_)) + def getBigDecimal(columnLabel: String): Option[BigDecimal] = getBigDecimal(columnNumber(columnLabel)) - def getOffsetDateTime(columnLabel: String): Option[OffsetDateTime] = Option(resultSet.getObject(columnLabel, classOf[OffsetDateTime])) + def getTime(column: Int): Option[Time] = getAs(column: Int, {item: Object => item.asInstanceOf[Time]}) + def getTime(columnLabel: String): Option[Time] = getTime(columnNumber(columnLabel)) - def getInstant(columnLabel: String): Option[Instant] = getOffsetDateTime(columnLabel).map(_.toInstant) + def getDate(column: Int): Option[Date] = getAs(column: Int, {item: Object => item.asInstanceOf[Date]}) + def getDate(columnLabel: String): Option[Date] = getDate(columnNumber(columnLabel)) - def getTime(columnLabel: String): Option[Time] = safe(resultSet.getTime(columnLabel)) + def getLocalDateTime(column: Int): Option[LocalDateTime] = getAs(column: Int, {item: Object => item.asInstanceOf[LocalDateTime]}) + def getLocalDateTime(columnLabel: String): Option[LocalDateTime] = getLocalDateTime(columnNumber(columnLabel)) - def getDate(columnLabel: String): Option[Date] = safe(resultSet.getDate(columnLabel)) + def getOffsetDateTime(column: Int): Option[OffsetDateTime] = getAs(column: Int, {item: Object => item.asInstanceOf[OffsetDateTime]}) + def getOffsetDateTime(columnLabel: String): Option[OffsetDateTime] = getOffsetDateTime(columnNumber(columnLabel)) - def getJsonB(columnLabel: String): Option[JsonBString] = { - Option(resultSet.getObject(columnLabel).asInstanceOf[PGobject])map(pgo => JsonBString(pgo.toString)) + def getInstant(column: Int): Option[Instant] = getOffsetDateTime(column).map(_.toInstant) + def getInstant(columnLabel: String): Option[Instant] = getOffsetDateTime(columnLabel).map(_.toInstant) + + def getUUID(column: Int): Option[UUID] = getAs(column: Int, {item: Object => item.asInstanceOf[UUID]}) + def getUUID(columnLabel: String): Option[UUID] = getUUID(columnNumber(columnLabel)) + + def getArray[T](column: Int): Option[Vector[T]] = { + def transformerFnc(obj: Object): Vector[T] = { + obj.asInstanceOf[sql.Array].getArray().asInstanceOf[Array[T]].toVector + } + getAs(column: Int, transformerFnc _) } - 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 getArray[T](columnLabel: String): Option[Vector[T]] = getArray[T](columnNumber(columnLabel)) + + def getArray[T](column: Int, itemTransformerFnc: TransformerFnc[T]): Option[Vector[T]] = { + def transformerFnc(obj: Object): Vector[T] = { + obj + .asInstanceOf[sql.Array] + .getArray() + .asInstanceOf[Array[Object]] + .toVector + .map(itemTransformerFnc) } + + getAs(column: Int, transformerFnc _) } - def getAs[T](columnLabel: String): Option[T] = { - val result = resultSet.getObject(columnLabel) - if (resultSet.wasNull()) { - None - } else { - val resultTyped = result.asInstanceOf[T] - Option(resultTyped) +} + +object QueryResultRow { + + type FieldNames = Map[String, Int] + type TransformerFnc[T] = Object => T + type Extractor = ResultSet => Option[Object] + type Extractors = Vector[Extractor] + + def apply(resultSet: ResultSet)(implicit fieldNames: FieldNames, extractors: Extractors): QueryResultRow = { + val fields = extractors.map(_(resultSet)) + new QueryResultRow(resultSet.getRow, fields, fieldNames) + } + + def fieldNamesFromMetadata(metaData: ResultSetMetaData): FieldNames = { + Range.inclusive(1, metaData.getColumnCount).map(i => metaData.getColumnName(i) -> i).toMap + } + + def createExtractors(metaData: ResultSetMetaData): Extractors = { + def generalExtractor(resultSet: ResultSet, column: Int): Option[Object] = Option(resultSet.getObject(column)) + + def timeTzExtractor(resultSet: ResultSet, column: Int): Option[Object] = Option(resultSet.getObject(column, classOf[OffsetTime])) + + def timestampExtractor(resultSet: ResultSet, column: Int): Option[Object] = Option(resultSet.getObject(column, classOf[LocalDateTime])) + + def timestampTzExtractor(resultSet: ResultSet, column: Int): Option[Object] = Option(resultSet.getObject(column, classOf[OffsetDateTime])) + + def arrayExtractor(resultSet: ResultSet, column: Int): Option[Object] = { + val array: sql.Array = resultSet.getArray(column) + Option(array) } + + def columnTypeName(column: Int): String = metaData.getColumnTypeName(column).toLowerCase() + + Range.inclusive(1, metaData.getColumnCount).map { column => + val extractor: Extractor = metaData.getColumnType(column) match { + case Types.TIME if columnTypeName(column) == "timetz" => timeTzExtractor(_, column) + case Types.TIMESTAMP if columnTypeName(column) == "timestamptz" => timestampTzExtractor(_, column) + case Types.TIMESTAMP => timestampExtractor(_, column) + case Types.ARRAY => arrayExtractor(_, column) + case _ => generalExtractor(_, column) + } + extractor + }.toVector } } diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala index 13a8b4a..b2340b8 100644 --- a/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/JsonBString.scala @@ -16,4 +16,5 @@ package za.co.absa.db.balta.classes.simple +@deprecated("Use SimpleJsonString instead", "0.2.0") case class JsonBString(value: String) extends AnyVal diff --git a/balta/src/main/scala/za/co/absa/db/balta/classes/simple/SimpleJsonString.scala b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/SimpleJsonString.scala new file mode 100644 index 0000000..d712c49 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/classes/simple/SimpleJsonString.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.db.balta.classes.simple + +/** + * A simple class to signal a JSON string is expected. No validation is performed. No extended comparison functionality + * is provided. + * @param value A JSON string. + */ +case class SimpleJsonString(value: String) extends AnyVal + diff --git a/balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala b/balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala new file mode 100644 index 0000000..5b19ab0 --- /dev/null +++ b/balta/src/main/scala/za/co/absa/db/balta/implicits/Postgres.scala @@ -0,0 +1,41 @@ +/* + * 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.postgresql.util.PGobject +import za.co.absa.db.balta.classes.QueryResultRow +import za.co.absa.db.balta.classes.simple.{JsonBString, SimpleJsonString} + +object Postgres { + implicit class PostgresRow(val row: QueryResultRow) extends AnyVal { + private def jsonBStringTransformer(obj: Object): JsonBString = JsonBString(obj.asInstanceOf[PGobject].toString) + private def simpleJsonStringTransformer(obj: Object): SimpleJsonString = SimpleJsonString(obj.asInstanceOf[PGobject].toString) + + @deprecated("Use getSimpleJsonString instead", "0.2.0") + def getJsonB(column: Int): Option[JsonBString] = row.getAs[JsonBString](column: Int, jsonBStringTransformer _) + @deprecated("Use getSimpleJsonString instead", "0.2.0") + def getJsonB(columnLabel: String): Option[JsonBString] = getJsonB(row.columnNumber(columnLabel)) + + def getSimpleJsonString(column: Int): Option[SimpleJsonString] = row.getAs[SimpleJsonString](column: Int, simpleJsonStringTransformer _) + def getSimpleJsonString(columnLabel: String): Option[SimpleJsonString] = getSimpleJsonString(row.columnNumber(columnLabel)) + + def getSJSArray(column: Int): Option[Vector[SimpleJsonString]] = + row.getArray(column: Int, item => SimpleJsonString(item.asInstanceOf[String])) + def getSJSArray(columnLabel: String): Option[Vector[SimpleJsonString]] = getSJSArray(row.columnNumber(columnLabel)) + + } +} diff --git a/balta/src/test/resources/database.properties b/balta/src/test/resources/database.properties new file mode 100644 index 0000000..6927c67 --- /dev/null +++ b/balta/src/test/resources/database.properties @@ -0,0 +1,8 @@ +# jdbc settings +test.jdbc.url=jdbc:postgresql://localhost:5432/mag_db + +test.jdbc.username=mag_owner +test.jdbc.password=changeme + +# debug - Hint: Use `.only` to run one selected test only. Test are not designed for parallel run. +test.persist.db=false diff --git a/balta/src/test/resources/db/postgres/01_db.ddl b/balta/src/test/resources/db/postgres/01_db.ddl new file mode 100644 index 0000000..b3f3aba --- /dev/null +++ b/balta/src/test/resources/db/postgres/01_db.ddl @@ -0,0 +1,6 @@ +CREATE DATABASE mag_db + WITH + OWNER = postgres + ENCODING = 'UTF8' + TABLESPACE = pg_default + CONNECTION LIMIT = -1; diff --git a/balta/src/test/resources/db/postgres/02_users.ddl b/balta/src/test/resources/db/postgres/02_users.ddl new file mode 100644 index 0000000..56d8354 --- /dev/null +++ b/balta/src/test/resources/db/postgres/02_users.ddl @@ -0,0 +1,9 @@ +CREATE ROLE mag_owner WITH + LOGIN + NOSUPERUSER + NOCREATEDB + NOCREATEROLE + INHERIT + NOREPLICATION + CONNECTION LIMIT -1 + PASSWORD 'changeme'; diff --git a/balta/src/test/resources/db/postgres/03_schema_testing.ddl b/balta/src/test/resources/db/postgres/03_schema_testing.ddl new file mode 100644 index 0000000..d01b823 --- /dev/null +++ b/balta/src/test/resources/db/postgres/03_schema_testing.ddl @@ -0,0 +1,2 @@ +CREATE SCHEMA IF NOT EXISTS testing + AUTHORIZATION mag_owner; diff --git a/balta/src/test/resources/db/postgres/04_testing.base_types.ddl b/balta/src/test/resources/db/postgres/04_testing.base_types.ddl new file mode 100644 index 0000000..a78ad5c --- /dev/null +++ b/balta/src/test/resources/db/postgres/04_testing.base_types.ddl @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS testing.base_types; + +CREATE TABLE IF NOT EXISTS testing.base_types +( + long_type bigint, + boolean_type boolean, + char_type "char", + string_type text COLLATE pg_catalog."default", + int_type integer, + double_type double precision, + float_type real, + bigdecimal_type numeric(25,10), + date_type date, + time_type time without time zone, + timestamp_type timestamp without time zone, + timestamptz_type timestamp with time zone, + uuid_type uuid, + array_int_type integer[] +); + +ALTER TABLE IF EXISTS testing.base_types + OWNER to mag_owner; diff --git a/balta/src/test/resources/db/postgres/05_testing._base_types_data.sql b/balta/src/test/resources/db/postgres/05_testing._base_types_data.sql new file mode 100644 index 0000000..5db1daf --- /dev/null +++ b/balta/src/test/resources/db/postgres/05_testing._base_types_data.sql @@ -0,0 +1,13 @@ +TRUNCATE testing.base_types; + +INSERT INTO testing.base_types( + long_type, boolean_type, char_type, string_type, int_type, double_type, float_type, bigdecimal_type, date_type, + time_type, timestamp_type, timestamptz_type, + uuid_type, array_int_type) +VALUES (1, true, 'a', 'hello world', 257, 3.14, 2.71, 123456789.0123456789, '2022-08-09'::date, + '10:12:15'::time, '2020-06-02 01:00:00'::timestamp without time zone, '2021-04-03 11:00:00 CET'::timestamp with time zone, + '090416f8-7da0-4598-844b-63659334e5b6'::UUID, '{1,2,3}'::INTEGER[]); + +INSERT INTO testing.base_types( + long_type) +VALUES (NULL); diff --git a/balta/src/test/resources/db/postgres/06_testing.pg_types.ddl b/balta/src/test/resources/db/postgres/06_testing.pg_types.ddl new file mode 100644 index 0000000..22e57d0 --- /dev/null +++ b/balta/src/test/resources/db/postgres/06_testing.pg_types.ddl @@ -0,0 +1,11 @@ +CREATE TABLE testing.pg_types +( + id bigint NOT NULL, + json_type json, + jsonb_type jsonb, + array_of_json_type json[], + PRIMARY KEY (id) +); + +ALTER TABLE IF EXISTS testing.pg_types + OWNER to mag_owner; diff --git a/balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql b/balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql new file mode 100644 index 0000000..7dfe29f --- /dev/null +++ b/balta/src/test/resources/db/postgres/07_testing_pg_types_data.sql @@ -0,0 +1,15 @@ +TRUNCATE testing.pg_types; + + +INSERT INTO testing.pg_types( + id, json_type, jsonb_type, array_of_json_type) +VALUES ( + 1, + '{"a": 1, "b": "Hello"}'::JSON, + '{"Hello" : "World"}'::JSONB, + array['{"a": 2, "body": "Hold the line!"}', '{"a": 3, "body": ""}', '{"a": 4}']::json[] + ); + + +INSERT INTO testing.pg_types(id) +VALUES (2); diff --git a/balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala b/balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala new file mode 100644 index 0000000..bace35c --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/classes/QueryResultRowIntegrationTests.scala @@ -0,0 +1,221 @@ +/* + * 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.classes + +import org.scalatest.funsuite.AnyFunSuiteLike +import za.co.absa.db.balta.testing.classes.DBTestingConnection + +import java.sql.{Date, ResultSetMetaData, Time} +import java.text.SimpleDateFormat +import java.time.{Instant, LocalDateTime, OffsetDateTime} +import java.util.UUID + +class QueryResultRowIntegrationTests extends AnyFunSuiteLike with DBTestingConnection{ + private val (tableRows: List[QueryResultRow], metadata: ResultSetMetaData) = DBTable("testing.base_types").all("long_type") { q => + (q.toList, q.resultSetMetaData) + } + + test("fieldNamesFromMetada") { + val result = QueryResultRow.fieldNamesFromMetadata(metadata) + + val expecedResult = Seq( + "long_type", + "boolean_type", + "char_type", + "string_type", + "int_type", + "double_type", + "float_type", + "bigdecimal_type", + "date_type", + "time_type", + "timestamp_type", + "timestamptz_type", + "uuid_type", + "array_int_type") + .zipWithIndex + .map(x => (x._1, x._2 + 1)) + .toMap + assert(result.size == 14) + assert(result == expecedResult) + } + + test("getLong") { + //first row + assert(tableRows.head.getLong(1).contains(1)) + assert(tableRows.head.getLong("long_type").contains(1)) + assert(tableRows.head.getAs[Long](1).contains(1)) + assert(tableRows.head.getAs[Long]("long_type").contains(1)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getLong(1).isEmpty) + assert(secondRow.getLong("long_type").isEmpty) + } + + test("getBoolean") { + //first row + assert(tableRows.head.getBoolean(2).contains(true)) + assert(tableRows.head.getBoolean("boolean_type").contains(true)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getBoolean(2).isEmpty) + assert(secondRow.getBoolean("boolean_type").isEmpty) + } + + test("getChar") { + //first row + assert(tableRows.head.getChar(3).contains('a')) + assert(tableRows.head.getChar("char_type").contains('a')) + assert(tableRows.head.getChar(4).contains('h')) + assert(tableRows.head.getChar("string_type").contains('h')) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getChar(3).isEmpty) + assert(secondRow.getChar("char_type").isEmpty) + } + + test("getString") { + //first row + assert(tableRows.head.getString(4).contains("hello world")) + assert(tableRows.head.getString("string_type").contains("hello world")) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getString(2).isEmpty) + assert(secondRow.getString("string_type").isEmpty) + } + + test("getInt") { + //first row + assert(tableRows.head.getInt(5).contains(257)) + assert(tableRows.head.getInt("int_type").contains(257)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getInt(5).isEmpty) + assert(secondRow.getInt("int_type").isEmpty) + } + + test("getDouble") { + //first row + assert(tableRows.head.getDouble(6).contains(3.14)) + assert(tableRows.head.getDouble("double_type").contains(3.14)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getDouble(6).isEmpty) + assert(secondRow.getDouble("double_type").isEmpty) + } + + test("getFloat") { + //first row + assert(tableRows.head.getFloat(7).contains(2.71F)) + assert(tableRows.head.getFloat("float_type").contains(2.71F)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getFloat(7).isEmpty) + assert(secondRow.getFloat("float_type").isEmpty) + } + + test("getBigDecimal") { + //first row + assert(tableRows.head.getBigDecimal(8).contains(BigDecimal("123456789.0123456789"))) + assert(tableRows.head.getBigDecimal("bigdecimal_type").contains(BigDecimal("123456789.0123456789"))) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getBigDecimal(8).isEmpty) + assert(secondRow.getBigDecimal("bigdecimal_type").isEmpty) + } + + test("getDate") { + //first row + val df = new SimpleDateFormat("yyyy-MM-dd"); + val expected = new Date(df.parse("2022-08-09").getTime) + assert(tableRows.head.getDate(9).contains(expected)) + assert(tableRows.head.getDate("date_type").contains(expected)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getDate(9).isEmpty) + assert(secondRow.getDate("date_type").isEmpty) + } + + test("getTime") { + //first row + val df = new SimpleDateFormat("hh:mm:ss") + val expected = new Time(df.parse("10:12:15").getTime) + assert(tableRows.head.getTime(10).contains(expected)) + assert(tableRows.head.getTime("time_type").contains(expected)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getTime(10).isEmpty) + assert(secondRow.getTime("time_type").isEmpty) + } + + + test("getLocalDateTime") { + //first row + val timestampString = "2020-06-02T01:00:00" + val expectedLocalDateTime = LocalDateTime.parse(s"$timestampString") + + assert(tableRows.head.getLocalDateTime(11).contains(expectedLocalDateTime)) + assert(tableRows.head.getLocalDateTime("timestamp_type").contains(expectedLocalDateTime)) + + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getLocalDateTime(11).isEmpty) + assert(secondRow.getLocalDateTime("timestamp_type").isEmpty) + } + + test("getOffsetDateTime and getInstant") { + //first row + val expectedOffsetDateTime = OffsetDateTime.parse("2021-04-03T11:00:00+01:00") + val expectedInstant = Instant.parse("2021-04-03T10:00:00.00Z") + + val resultOfColumn = tableRows.head.getOffsetDateTime(12).get + val resultOfColumnLabel = tableRows.head.getOffsetDateTime("timestamptz_type").get + assert(resultOfColumn.isEqual(expectedOffsetDateTime)) + assert(resultOfColumnLabel.isEqual(expectedOffsetDateTime)) + assert(tableRows.head.getInstant(12).contains(expectedInstant)) + assert(tableRows.head.getInstant("timestamptz_type").contains(expectedInstant)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getOffsetDateTime(12).isEmpty) + assert(secondRow.getOffsetDateTime("timestamptz_type").isEmpty) + assert(secondRow.getInstant(12).isEmpty) + assert(secondRow.getInstant("timestamptz_type").isEmpty) + } + + test("getUUID") { + //first row + val expected = UUID.fromString("090416f8-7da0-4598-844b-63659334e5b6") + assert(tableRows.head.getUUID(13).contains(expected)) + assert(tableRows.head.getUUID("uuid_type").contains(expected)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getUUID(13).isEmpty) + assert(secondRow.getUUID("uuid_type").isEmpty) + } + + test("getArray[Int]") { + //first row + val expected = Vector(1, 2, 3) + assert(tableRows.head.getArray[Int](14).contains(expected)) + assert(tableRows.head.getArray[Int]("array_int_type").contains(expected)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getArray[Int](14).isEmpty) + assert(secondRow.getArray[Int]("array_int_type").isEmpty) + } + +} diff --git a/balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala b/balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala new file mode 100644 index 0000000..ad97888 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/implicits/PostgresRowIntegrationTests.scala @@ -0,0 +1,78 @@ +/* + * 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.classes.{DBTable, QueryResultRow} +import za.co.absa.db.balta.testing.classes.DBTestingConnection + +import java.sql.ResultSetMetaData +import Postgres.PostgresRow +import za.co.absa.db.balta.classes.simple.SimpleJsonString + +class PostgresRowIntegrationTests extends AnyFunSuiteLike with DBTestingConnection{ + private val (tableRows: List[QueryResultRow], metadata: ResultSetMetaData) = DBTable("testing.pg_types").all("id") { q => + (q.toList, q.resultSetMetaData) + } + + test("fieldNamesFromMetada") { + val result = QueryResultRow.fieldNamesFromMetadata(metadata) + + val expectedResult = Seq( + "id", + "json_type", + "jsonb_type", + "array_of_json_type" + ) + .zipWithIndex + .map(x => (x._1, x._2 + 1)) + .toMap + assert(result.size == 4) + assert(result == expectedResult) + } + + test("getSimpleJson") { + //first row + val expectedJson = SimpleJsonString("""{"a": 1, "b": "Hello"}""") + val expectedJsonB = SimpleJsonString("""{"Hello": "World"}""") + assert(tableRows.head.getSimpleJsonString(2).contains(expectedJson)) + assert(tableRows.head.getSimpleJsonString("json_type").contains(expectedJson)) + assert(tableRows.head.getSimpleJsonString(3).contains(expectedJsonB)) + assert(tableRows.head.getSimpleJsonString("jsonb_type").contains(expectedJsonB)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getSimpleJsonString(2).isEmpty) + assert(secondRow.getSimpleJsonString("json_type").isEmpty) + assert(secondRow.getSimpleJsonString(3).isEmpty) + assert(secondRow.getSimpleJsonString("jsonb_type").isEmpty) + } + + test("getArrayJson") { + //first row + val expected = Vector( + SimpleJsonString("""{"a": 2, "body": "Hold the line!"}"""), + SimpleJsonString("""{"a": 3, "body": ""}"""), + SimpleJsonString("""{"a": 4}""") + ) + assert(tableRows.head.getSJSArray(4).get == expected) + assert(tableRows.head.getSJSArray("array_of_json_type").contains(expected)) + //second row + val secondRow = tableRows.tail.head + assert(secondRow.getSJSArray(4).isEmpty) + assert(secondRow.getSJSArray("array_of_json_type").isEmpty) + } +} diff --git a/balta/src/test/scala/za/co/absa/db/balta/testing/classes/DBTestingConnection.scala b/balta/src/test/scala/za/co/absa/db/balta/testing/classes/DBTestingConnection.scala new file mode 100644 index 0000000..f604639 --- /dev/null +++ b/balta/src/test/scala/za/co/absa/db/balta/testing/classes/DBTestingConnection.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.testing.classes + +import za.co.absa.db.balta.classes.DBConnection +import za.co.absa.db.balta.classes.simple.ConnectionInfo + +import java.util.Properties + +trait DBTestingConnection { + lazy implicit val connection: DBConnection = DBConnection(connectionInfo) + + protected lazy val connectionInfo: ConnectionInfo = readConnectionInfoFromConfig + + private def readConnectionInfoFromConfig: ConnectionInfo = { + 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/build.sbt b/build.sbt index 52093c3..7e9bcfe 100644 --- a/build.sbt +++ b/build.sbt @@ -22,6 +22,8 @@ lazy val scala213 = "2.13.11" lazy val supportedScalaVersions: Seq[String] = Seq(scala211, scala212 , scala213) +name := "balta" + ThisBuild / scalaVersion := scala212 ThisBuild / versionScheme := Some("early-semver") From 5b334af35be4460bc9ecd9002120220dadfe0aa6 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:24:17 +0200 Subject: [PATCH 6/7] #31: Release v0.2.0 failed (#32) * sonatypeProfileName property added --- publish.sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.sbt b/publish.sbt index d7d9e55..9ee92f1 100644 --- a/publish.sbt +++ b/publish.sbt @@ -50,6 +50,7 @@ ThisBuild / developers := List( ) ThisBuild / organization := "za.co.absa.db" +sonatypeProfileName := "za.co.absa" ThisBuild / organizationName := "ABSA Group Limited" ThisBuild / organizationHomepage := Some(url("https://www.absa.africa")) From a63dc258518807dbf9e14f5c3754a9ff3a749773 Mon Sep 17 00:00:00 2001 From: David Benedeki <14905969+benedeki@users.noreply.github.com> Date: Fri, 27 Sep 2024 22:45:29 +0200 Subject: [PATCH 7/7] #22: Add Spike template (#40) --- .github/ISSUE_TEMPLATE/spike_task.md | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/spike_task.md diff --git a/.github/ISSUE_TEMPLATE/spike_task.md b/.github/ISSUE_TEMPLATE/spike_task.md new file mode 100644 index 0000000..d071e81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/spike_task.md @@ -0,0 +1,36 @@ +--- +name: Spike +about: Issue template for spikes, research and investigation tasks +labels: 'spike' + +--- + +## Background +A clear and concise description of the problem or a topic we need to understand. + +Feel free to add information about why it's needed and what assumptions you have at the moment. + +## Questions To Answer + +1. +2. +3. + +## Desired Outcome + +The list of desired outcomes of this spike ticket. + +```[tasklist] +### Tasks +- [ ] Questions have been answered or we have a clearer idea of how to get to our goal +- [ ] Discussion with the team +- [ ] Documentation +- [ ] Create recommendations and new implementation tickets +- [ ] item here.. +``` + +## Additional Info/Resources [Optional] + +1. +2. +3.