diff --git a/core/src/main/scala/za/co/absa/fadb/DBEngine.scala b/core/src/main/scala/za/co/absa/fadb/DBEngine.scala new file mode 100644 index 00000000..18fa47e2 --- /dev/null +++ b/core/src/main/scala/za/co/absa/fadb/DBEngine.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.language.higherKinds + +/** + * A basis to represent a database executor + */ +trait DBEngine { + + /** + * A type representing the (SQL) query within the engine + * @tparam T - the return type of the query + */ + type QueryType[T] <: Query[T] + + /** + * The actual query executioner of the queries of the engine + * @param query - the query to execute + * @tparam R - return the of the query + * @return - sequence of the results of database query + */ + protected def run[R](query: QueryType[R]): Future[Seq[R]] + + /** + * Public method to execute when query is expected to return multiple results + * @param query - the query to execute + * @tparam R - return the of the query + * @return - sequence of the results of database query + */ + def execute[R](query: QueryType[R]): Future[Seq[R]] = run(query) + + /** + * Public method to execute when query is expected to return exactly one row + * @param query - the query to execute + * @tparam R - return the of the query + * @return - sequence of the results of database query + */ + def unique[R](query: QueryType[R]): Future[R] = { + run(query).map(_.head) + } + + /** + * Public method to execute when query is expected to return one or no results + * @param query - the query to execute + * @tparam R - return the of the query + * @return - sequence of the results of database query + */ + + def option[R](query: QueryType[R]): Future[Option[R]] = { + run(query).map(_.headOption) + } +} + diff --git a/core/src/main/scala/za/co/absa/fadb/DBFunction.scala b/core/src/main/scala/za/co/absa/fadb/DBFunction.scala index d6432de3..7b842fec 100644 --- a/core/src/main/scala/za/co/absa/fadb/DBFunction.scala +++ b/core/src/main/scala/za/co/absa/fadb/DBFunction.scala @@ -16,20 +16,57 @@ package za.co.absa.fadb +import za.co.absa.fadb.naming_conventions.NamingConvention + import scala.concurrent.Future /** - * The most general abstraction of database function representation - * The database name of the function is derives from the class name based on the provided naming convention (in schema) * - * @param schema - the schema the function belongs into * @param functionNameOverride - in case the class name would not match the database function name, this gives the * possibility of override - * @tparam E - the type of the [[DBExecutor]] engine - * @tparam T - the type covering the input fields of the database function + * @param schema - the schema the function belongs into + * @param dBEngine - the database engine that is supposed to execute the function (presumably contains + * connection to the database + * @tparam I - the type covering the input fields of the database function * @tparam R - the type covering the returned fields from the database function + * @tparam E - the type of the [[DBEngine]] engine */ -abstract class DBFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) extends DBFunctionFabric { +abstract class DBFunction[I, R, E <: DBEngine](functionNameOverride: Option[String] = None) + (implicit val schema: DBSchema, val dBEngine: E) extends DBFunctionFabric { + + /* alternative constructors for different availability of input parameters */ + def this(schema: DBSchema, functionNameOverride: String) + (implicit dBEngine: E) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + /* only one constructor of a class can have default values for parameters*/ + def this(schema: DBSchema) + (implicit dBEngine: E) = { + this(None)(schema, dBEngine) + } + + def this(dBEngine: E, functionNameOverride: String) + (implicit schema: DBSchema) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(dBEngine: E) + (implicit schema: DBSchema) = { + this(None)(schema, dBEngine) + } + + /** + * Function to create the DB function call specific to the provided [[DBEngine]]. Expected to be implemented by the + * DBEngine specific mix-in. + * @param values - the values to pass over to the database function + * @return - the SQL query in the format specific to the provided [[DBEngine]] + */ + protected def query(values: I): dBEngine.QueryType[R] + + /** + * Name of the function, based on the class name, unless it is overridden in the constructor + */ val functionName: String = { val fn = functionNameOverride.getOrElse(schema.objectNameFromClassName(getClass)) if (schema.schemaName.isEmpty) { @@ -39,65 +76,149 @@ abstract class DBFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Op } } + def namingConvention: NamingConvention = schema.namingConvention + /** - * For the given output it returns a function to execute the SQL query and interpret the results. - * Basically it should create a function which contains a query to be executable and executed on on the [[DBExecutor]] - * and transforming the result of that query to result type. - * @param values - the input values of the DB function (stored procedure) - * @return - the query function that when provided an executor will return the result of the DB function call + * List of fields to select from the DB function. Expected to be based on the return type `R` + * @return - list of fields to select */ - protected def queryFunction(values: T): QueryFunction[E, R] + override protected def fieldsToSelect: Seq[String] = super.fieldsToSelect //TODO should get the names from R #6 + + /*these 3 functions has to be defined here and not in the ancestors, as there the query type is not compatible - path-dependent types*/ + protected def execute(values: I): Future[Seq[R]] = dBEngine.execute[R](query(values)) + protected def unique(values: I): Future[R] = dBEngine.unique(query(values)) + protected def option(values: I): Future[Option[R]] = dBEngine.option(query(values)) + } object DBFunction { /** * Represents a function returning a set (in DB sense) of rows - * - * @param schema - the schema the function belongs into * @param functionNameOverride - in case the class name would not match the database function name, this gives the * possibility of override - * @tparam E - the type of the [[DBExecutor]] engine - * @tparam T - the type covering the input fields of the database function + * @param schema - the schema the function belongs into + * @param dBEngine - the database engine that is supposed to execute the function (presumably contains + * connection to the database + * @tparam I - the type covering the input fields of the database function * @tparam R - the type covering the returned fields from the database function + * @tparam E - the type of the [[DBEngine]] engine */ - abstract class DBSeqFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) - extends DBFunction[E, T, R](schema, functionNameOverride) { - def apply(values: T): Future[Seq[R]] = { - schema.execute(queryFunction(values)) + abstract class DBSeqFunction[I, R, E <: DBEngine](functionNameOverride: Option[String] = None) + (implicit schema: DBSchema, dBEngine: E) + extends DBFunction[I, R, E](functionNameOverride) { + + def this(schema: DBSchema, functionNameOverride: String) + (implicit dBEngine: E) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(schema: DBSchema) + (implicit dBEngine: E) = { + this(None)(schema, dBEngine) } + + def this(dBEngine: E, functionNameOverride: String) + (implicit schema: DBSchema) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(dBEngine: E) + (implicit schema: DBSchema) = { + this(None)(schema, dBEngine) + } + + /** + * For easy and convenient execution of the DB function call + * @param values - the values to pass over to the database function + * @return - a sequence of values, each coming from a row returned from the DB function transformed to scala + * type `R` + */ + def apply(values: I): Future[Seq[R]] = execute(values) } /** * Represents a function returning exactly one record - * - * @param schema - the schema the function belongs into * @param functionNameOverride - in case the class name would not match the database function name, this gives the * possibility of override - * @tparam E - the type of the [[DBExecutor]] engine - * @tparam T - the type covering the input fields of the database function + * @param schema - the schema the function belongs into + * @param dBEngine - the database engine that is supposed to execute the function (presumably contains + * connection to the database + * @tparam I - the type covering the input fields of the database function * @tparam R - the type covering the returned fields from the database function + * @tparam E - the type of the [[DBEngine]] engine */ - abstract class DBUniqueFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) - extends DBFunction[E, T, R](schema, functionNameOverride) { - def apply(values: T): Future[R] = { - schema.unique(queryFunction(values)) + abstract class DBUniqueFunction[I, R, E <: DBEngine](functionNameOverride: Option[String] = None) + (implicit schema: DBSchema, dBEngine: E) + extends DBFunction[I, R, E](functionNameOverride) { + + def this(schema: DBSchema, functionNameOverride: String) + (implicit dBEngine: E) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(schema: DBSchema) + (implicit dBEngine: E) = { + this(None)(schema, dBEngine) } + + def this(dBEngine: E, functionNameOverride: String) + (implicit schema: DBSchema) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(dBEngine: E) + (implicit schema: DBSchema) = { + this(None)(schema, dBEngine) + } + + /** + * For easy and convenient execution of the DB function call + * @param values - the values to pass over to the database function + * @return - the value returned from the DB function transformed to scala type `R` + */ + def apply(values: I): Future[R] = unique(values) } /** * Represents a function returning one optional record - * - * @param schema - the schema the function belongs into * @param functionNameOverride - in case the class name would not match the database function name, this gives the * possibility of override - * @tparam E - the type of the [[DBExecutor]] engine - * @tparam T - the type covering the input fields of the database function + * @param schema - the schema the function belongs into + * @param dBEngine - the database engine that is supposed to execute the function (presumably contains + * connection to the database + * @tparam I - the type covering the input fields of the database function * @tparam R - the type covering the returned fields from the database function + * @tparam E - the type of the [[DBEngine]] engine */ - abstract class DBOptionFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) - extends DBFunction[E, T, R](schema, functionNameOverride) { - def apply(values: T): Future[Option[R]] = { - schema.option(queryFunction(values)) + abstract class DBOptionFunction[I, R, E <: DBEngine](functionNameOverride: Option[String] = None) + (implicit schema: DBSchema, dBEngine: E) + extends DBFunction[I, R, E](functionNameOverride) { + + def this(schema: DBSchema, functionNameOverride: String) + (implicit dBEngine: E) = { + this(Option(functionNameOverride))(schema, dBEngine) + } + + def this(schema: DBSchema) + (implicit dBEngine: E) = { + this(None)(schema, dBEngine) + } + + def this(dBEngine: E, functionNameOverride: String) + (implicit schema: DBSchema) = { + this(Option(functionNameOverride))(schema, dBEngine) } + + def this(dBEngine: E) + (implicit schema: DBSchema) = { + this(None)(schema, dBEngine) + } + + /** + * For easy and convenient execution of the DB function call + * @param values - the values to pass over to the database function + * @return - the value returned from the DB function transformed to scala type `R` if a row is returned, otherwise `None` + */ + def apply(values: I): Future[Option[R]] = option(values) } } diff --git a/core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala b/core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala index c2fb5276..5d4fc55e 100644 --- a/core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala +++ b/core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala @@ -21,7 +21,15 @@ package za.co.absa.fadb * that offer certain implementations. This trait should help with the inheritance of all of these */ trait DBFunctionFabric { + + /** + * Name of the function the class represents + */ def functionName: String + /** + * List of fields to select from the DB function. + * @return - list of fields to select + */ protected def fieldsToSelect: Seq[String] = Seq.empty } diff --git a/core/src/main/scala/za/co/absa/fadb/DBSchema.scala b/core/src/main/scala/za/co/absa/fadb/DBSchema.scala index e372c107..6681a388 100644 --- a/core/src/main/scala/za/co/absa/fadb/DBSchema.scala +++ b/core/src/main/scala/za/co/absa/fadb/DBSchema.scala @@ -18,42 +18,56 @@ package za.co.absa.fadb import za.co.absa.fadb.naming_conventions.NamingConvention -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - /** * An abstract class, an ancestor to represent a database schema (each database function should be placed in a schema) - * The database name of the schema is derives from the class name based on the provided naming convention - * - * @param executor - executor to execute the queries through + * The database name of the schema is derived from the class name based on the provided naming convention * @param schemaNameOverride - in case the class name would not match the database schema name, this gives the * possibility of override - * @param namingConvention - the [[za.co.absa.fadb.naming_conventions.NamingConvention]](NamingConvention) prescribing how to convert a class name into a db object name - * @tparam E - the engine of the executor type, e.g. Slick Database + * @param dBEngine - [[DBEngine]] to execute the functions with. Not directly needed for the DBSchema class, rather + * to be passed on to [[DBFunction]] members of the schema + * @param namingConvention - the [[za.co.absa.fadb.naming_conventions.NamingConvention NamingConvention]] prescribing how to convert a class name into a db object name */ -abstract class DBSchema[E](val executor: DBExecutor[E], schemaNameOverride: Option[String] = None) - (implicit namingConvention: NamingConvention) { - +abstract class DBSchema(schemaNameOverride: Option[String] = None) + (implicit dBEngine: DBEngine, implicit val namingConvention: NamingConvention) { - def objectNameFromClassName(c: Class[_]): String = { - namingConvention.fromClassNamePerConvention(c) + def this(dBEngine: DBEngine, schemaNameOverride: String) + (implicit namingConvention: NamingConvention) { + this(Option(schemaNameOverride))(dBEngine, namingConvention) } - val schemaName: String = schemaNameOverride.getOrElse(objectNameFromClassName(getClass)) + def this(dBEngine: DBEngine) + (implicit namingConvention: NamingConvention) { + this(None)(dBEngine, namingConvention) + } - def execute[R](query: QueryFunction[E, R]): Future[Seq[R]] = { - executor.run(query) + def this(namingConvention: NamingConvention, schemaNameOverride:String) + (implicit dBEngine: DBEngine) { + this(Option(schemaNameOverride))(dBEngine, namingConvention) } - def unique[R](query: QueryFunction[E, R]): Future[R] = { - for { - all <- execute(query) - } yield all.head + def this(namingConvention: NamingConvention) + (implicit dBEngine: DBEngine) { + this(None)(dBEngine, namingConvention) } - def option[R](query: QueryFunction[E, R]): Future[Option[R]] = { - for { - all <- execute(query) - } yield all.headOption + /** + * To easy pass over to [[DBFunction]] members of the schema + */ + protected implicit val schema: DBSchema = this + + /** + * Function to convert a class to the associated DB object name, based on the class' name. For transformation from the + * class name to usual db name the schema's [[za.co.absa.fadb.naming_conventions.NamingConvention NamingConvention]] is used. + * @param c - class which name to use to get the DB object name + * @return - the db object name + */ + def objectNameFromClassName(c: Class[_]): String = { + namingConvention.fromClassNamePerConvention(c) } + + /** + * Name of the schema. Based on the schema's class name or provided override + */ + val schemaName: String = schemaNameOverride.getOrElse(objectNameFromClassName(getClass)) + } diff --git a/core/src/main/scala/za/co/absa/fadb/DBExecutor.scala b/core/src/main/scala/za/co/absa/fadb/Query.scala similarity index 67% rename from core/src/main/scala/za/co/absa/fadb/DBExecutor.scala rename to core/src/main/scala/za/co/absa/fadb/Query.scala index 24431b22..79fdb6a4 100644 --- a/core/src/main/scala/za/co/absa/fadb/DBExecutor.scala +++ b/core/src/main/scala/za/co/absa/fadb/Query.scala @@ -16,13 +16,8 @@ package za.co.absa.fadb -import scala.concurrent.Future - /** - * And abstraction to make it possible to execute queries through regardless of the provided database engine library - * - * @tparam E - the type of the engine, E.g. a Slick Postgres Database + * The basis for all query types of [[DBEngine]] implementations + * @tparam R - the return type of the query */ -trait DBExecutor[E] { - def run[R](fnc: QueryFunction[E, R]): Future[Seq[R]] -} +trait Query[R] diff --git a/core/src/main/scala/za/co/absa/fadb/exceptions/DBFailException.scala b/core/src/main/scala/za/co/absa/fadb/exceptions/DBFailException.scala index c390cf0d..6f693143 100644 --- a/core/src/main/scala/za/co/absa/fadb/exceptions/DBFailException.scala +++ b/core/src/main/scala/za/co/absa/fadb/exceptions/DBFailException.scala @@ -20,7 +20,14 @@ package za.co.absa.fadb.exceptions * General Fa-DB exception class * @param message - the message describing the reason of exception */ -class DBFailException(message: String) extends Exception(message) +class DBFailException(message: String) extends Exception(message) { + override def equals(obj: Any): Boolean = { + obj match { + case other: DBFailException => (other.getMessage == message) && (getClass == other.getClass) + case _ => false + } + } +} object DBFailException { def apply(message: String): DBFailException = new DBFailException(message) diff --git a/core/src/main/scala/za/co/absa/fadb/package.scala b/core/src/main/scala/za/co/absa/fadb/statushandling/FunctionStatus.scala similarity index 56% rename from core/src/main/scala/za/co/absa/fadb/package.scala rename to core/src/main/scala/za/co/absa/fadb/statushandling/FunctionStatus.scala index 14f51ef0..03eee520 100644 --- a/core/src/main/scala/za/co/absa/fadb/package.scala +++ b/core/src/main/scala/za/co/absa/fadb/statushandling/FunctionStatus.scala @@ -14,16 +14,11 @@ * limitations under the License. */ -package za.co.absa +package za.co.absa.fadb.statushandling -import scala.concurrent.Future - -package object fadb { - /** - * Represents a database query call (in the model of Fa-Db a call to a DB stored procedure). When provided a DB - * connection (of type [[DBExecutor]]) it executes the query and transforms it to the desired result type sequence. - * @tparam E - the type of the DB connection to execute on - * @tparam R - the type of result - */ - type QueryFunction[E, R] = E => Future[Seq[R]] -} +/** + * Class represents the status of calling a fa-db function (if it supports status that is) + * @param statusCode - status code identifying if the function call succeeded or failed and how + * @param statusText - human readable description of the status returned + */ +case class FunctionStatus(statusCode: Int, statusText: String) diff --git a/core/src/main/scala/za/co/absa/fadb/statushandling/StatusException.scala b/core/src/main/scala/za/co/absa/fadb/statushandling/StatusException.scala index aa068500..01647e8f 100644 --- a/core/src/main/scala/za/co/absa/fadb/statushandling/StatusException.scala +++ b/core/src/main/scala/za/co/absa/fadb/statushandling/StatusException.scala @@ -20,22 +20,56 @@ import za.co.absa.fadb.exceptions.DBFailException /** * Exception caused by status signaling a failure in DB function execution - * @param status - the status that caused the error - * @param statusText - the status text explaining the status code + * @param status - represent the status information returned from the function call */ -class StatusException(val status: Int, statusText: String) extends DBFailException(statusText) { - def statusText: String = getMessage +class StatusException(val status:FunctionStatus) extends DBFailException(status.statusText) { + + override def equals(obj: Any): Boolean = { + obj match { + case other: StatusException => (other.status == status) && (getClass == other.getClass) + case _ => false + } + } } object StatusException { - class ServerMisconfigurationException(status: Int, statusText: String) extends StatusException(status, statusText) - class DataConflictException(status: Int, statusText: String) extends StatusException(status, statusText) + def apply(status: FunctionStatus): StatusException = new StatusException(status) + def apply(status: Int, statusText: String): StatusException = new StatusException(FunctionStatus(status, statusText)) + + class ServerMisconfigurationException(status:FunctionStatus) extends StatusException(status) + + class DataConflictException(status:FunctionStatus) extends StatusException(status) + + class DataNotFoundException(status:FunctionStatus) extends StatusException(status) + + class ErrorInDataException(status:FunctionStatus) extends StatusException(status) + + class OtherStatusException(status:FunctionStatus) extends StatusException(status) + + object ServerMisconfigurationException { + def apply(status: FunctionStatus): ServerMisconfigurationException = new ServerMisconfigurationException(status) + def apply(status: Int, statusText: String): ServerMisconfigurationException = new ServerMisconfigurationException(FunctionStatus(status, statusText)) + } + + object DataConflictException { + def apply(status: FunctionStatus): DataConflictException = new DataConflictException(status) + def apply(status: Int, statusText: String): DataConflictException = new DataConflictException(FunctionStatus(status, statusText)) + } - class DataNotFoundException(status: Int, statusText: String) extends StatusException(status, statusText) + object DataNotFoundException { + def apply(status: FunctionStatus): DataNotFoundException = new DataNotFoundException(status) + def apply(status: Int, statusText: String): DataNotFoundException = new DataNotFoundException(FunctionStatus(status, statusText)) + } - class ErrorInDataException(status: Int, statusText: String) extends StatusException(status, statusText) + object ErrorInDataException { + def apply(status: FunctionStatus): ErrorInDataException = new ErrorInDataException(status) + def apply(status: Int, statusText: String): ErrorInDataException = new ErrorInDataException(FunctionStatus(status, statusText)) + } - class OtherStatusException(status: Int, statusText: String) extends StatusException(status, statusText) + object OtherStatusException { + def apply(status: FunctionStatus): OtherStatusException = new OtherStatusException(status) + def apply(status: Int, statusText: String): OtherStatusException = new OtherStatusException(FunctionStatus(status, statusText)) + } } diff --git a/core/src/main/scala/za/co/absa/fadb/statushandling/StatusHandling.scala b/core/src/main/scala/za/co/absa/fadb/statushandling/StatusHandling.scala index 30e0c865..453a946b 100644 --- a/core/src/main/scala/za/co/absa/fadb/statushandling/StatusHandling.scala +++ b/core/src/main/scala/za/co/absa/fadb/statushandling/StatusHandling.scala @@ -17,26 +17,46 @@ package za.co.absa.fadb.statushandling import za.co.absa.fadb.DBFunctionFabric -import za.co.absa.fadb.statushandling.StatusHandling.{defaultStatusFieldName, defaultStatusTextFieldName} +import za.co.absa.fadb.naming_conventions.NamingConvention +import za.co.absa.fadb.statushandling.StatusHandling.{defaultStatusField, defaultStatusTextField} import scala.util.Try /** - * A basis for mix-in traits for [[DBFunction]] that support `status` and `status_text` for easier handling + * A basis for mix-in traits for [[za.co.absa.fadb.DBFunction DBFunction]] that support `status` and `status text` for easier handling */ -trait StatusHandling extends DBFunctionFabric{ - - def statusFieldName: String = defaultStatusFieldName - def statusTextFieldName: String = defaultStatusTextFieldName - +trait StatusHandling extends DBFunctionFabric { + + /** + * @return - the naming convention to use when converting the internal status and status text fields to DB fields + */ + def namingConvention: NamingConvention + + /** + * Verifies if the given status means success or failure + * @param status - the status to check + * @return - Success or failure the status means + */ + protected def checkStatus(status: FunctionStatus): Try[FunctionStatus] + protected def checkStatus(status: Integer, statusText: String): Try[FunctionStatus] = checkStatus(FunctionStatus(status, statusText)) + + def statusField: String = defaultStatusField + def statusTextField: String = defaultStatusTextField + + /** + * A mix-in to add the status fields into the SELECT statement + * @return a sequence of fields to use in SELECT + */ override protected def fieldsToSelect: Seq[String] = { - Seq(statusFieldName, statusTextFieldName) ++ super.fieldsToSelect + Seq( + namingConvention.stringPerConvention(statusField), + namingConvention.stringPerConvention(statusTextField) + ) ++ super.fieldsToSelect } - protected def checkStatus(status: Integer, statusTex: String): Try[Unit] } object StatusHandling { - val defaultStatusFieldName = "status" - val defaultStatusTextFieldName = "status_text" + val defaultStatusField = "status" + val defaultStatusTextField = "statusText" } diff --git a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgExecutor.scala b/core/src/main/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandling.scala similarity index 57% rename from slick/src/main/scala/za/co/absa/fadb/slick/SlickPgExecutor.scala rename to core/src/main/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandling.scala index e9d8f1d7..c49f64ce 100644 --- a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgExecutor.scala +++ b/core/src/main/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandling.scala @@ -14,23 +14,21 @@ * limitations under the License. */ -package za.co.absa.fadb.slick +package za.co.absa.fadb.statushandling +import scala.util.{Failure, Success, Try} -import scala.concurrent.Future -import slick.jdbc.PostgresProfile.api._ -import za.co.absa.fadb.{DBExecutor, QueryFunction} +/** + * + */ +trait UserDefinedStatusHandling extends StatusHandling { + def OKStatuses: Set[Integer] - -class SlickPgExecutor(db: Database) extends DBExecutor[Database] { - override def run[R](fnc: QueryFunction[Database, R]): Future[Seq[R]] = { - fnc(db) - } -} - -object SlickPgExecutor { - def forConfig(dbConfig: String): SlickPgExecutor = { - val db = Database.forConfig(dbConfig) - new SlickPgExecutor(db) + def checkStatus(status: FunctionStatus): Try[FunctionStatus] = { + if (OKStatuses.contains(status.statusCode)) { + Success(status) + } else { + Failure(StatusException(status)) + } } } diff --git a/core/src/main/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandling.scala b/core/src/main/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandling.scala index 147be3fc..23957508 100644 --- a/core/src/main/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandling.scala +++ b/core/src/main/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandling.scala @@ -17,24 +17,24 @@ package za.co.absa.fadb.statushandling.fadbstandard import za.co.absa.fadb.exceptions.DBFailException -import za.co.absa.fadb.statushandling.StatusHandling +import za.co.absa.fadb.statushandling.{FunctionStatus, StatusHandling} import za.co.absa.fadb.statushandling.StatusException._ import scala.util.{Failure, Success, Try} /** - * A mix in trait for [[DBFunction]] for standard handling of `status` and `status_text` fields. + * A mix-in trait for [[za.co.absa.fadb.DBFunction DBFunction]] for standard handling of `status` and `statusText` fields. */ trait StandardStatusHandling extends StatusHandling { - override protected def checkStatus(status: Integer, statusText: String): Try[Unit] = { - status / 10 match { - case 1 => Success(Unit) - case 2 => Failure(new ServerMisconfigurationException(status, statusText)) - case 3 => Failure(new DataConflictException(status, statusText)) - case 4 => Failure(new DataNotFoundException(status, statusText)) - case 5 | 6 | 7 | 8 => Failure(new ErrorInDataException(status, statusText)) - case 9 => Failure(new OtherStatusException(status, statusText)) - case _ => Failure(DBFailException(s"Status out of range - with status: $status and status text: '$statusText'")) + override protected def checkStatus(status: FunctionStatus): Try[FunctionStatus] = { + status.statusCode / 10 match { + case 1 => Success(status) + case 2 => Failure(ServerMisconfigurationException(status)) + case 3 => Failure(DataConflictException(status)) + case 4 => Failure(DataNotFoundException(status)) + case 5 | 6 | 7 | 8 => Failure(ErrorInDataException(status)) + case 9 => Failure(OtherStatusException(status)) + case _ => Failure(DBFailException(s"Status out of range - with status: ${status.statusCode} and status text: '${status.statusText}'")) } } } diff --git a/core/src/test/scala/za/co/absa/fadb/DBFunctionSuite.scala b/core/src/test/scala/za/co/absa/fadb/DBFunctionSuite.scala index f50332de..b55d0bbf 100644 --- a/core/src/test/scala/za/co/absa/fadb/DBFunctionSuite.scala +++ b/core/src/test/scala/za/co/absa/fadb/DBFunctionSuite.scala @@ -21,23 +21,23 @@ import org.scalatest.funsuite.AnyFunSuite import scala.concurrent.Future import za.co.absa.fadb.naming_conventions.SnakeCaseNaming.Implicits.namingConvention + class DBFunctionSuite extends AnyFunSuite { - private type Engine = String // just an engine type, not relevant Here - private object ExecutorThrow extends DBExecutor[String] { - override def run[R](fnc: QueryFunction[Engine, R]): Future[Seq[R]] = { - throw new Exception("Should never get here") - } + private def neverHappens: Nothing = { + throw new Exception("Should never get here") + } + + private implicit object EngineThrow extends DBEngine { + override def run[R](query: QueryType[R]): Future[Seq[R]] = neverHappens } - private object FooNamed extends DBSchema(ExecutorThrow) - private object FooNameless extends DBSchema(ExecutorThrow, Some("")) + private object FooNamed extends DBSchema(EngineThrow) + private object FooNameless extends DBSchema(EngineThrow, "") test("Function name check"){ - case class MyFunction(schema: DBSchema[Engine]) extends DBFunction(schema) { - override protected def queryFunction(values: Nothing): QueryFunction[Engine, Nothing] = { - throw new Exception("Should never get here") - } + case class MyFunction(override val schema: DBSchema) extends DBFunction[Unit, Unit, DBEngine](schema) { + override protected def query(values: Unit): dBEngine.QueryType[Unit] = neverHappens } val fnc1 = MyFunction(FooNamed) @@ -48,10 +48,8 @@ class DBFunctionSuite extends AnyFunSuite { } test("Function name override check"){ - case class MyFunction(schema: DBSchema[Engine]) extends DBFunction(schema, Some("bar")) { - override protected def queryFunction(values: Nothing): QueryFunction[Engine, Nothing] = { - throw new Exception("Should never get here") - } + case class MyFunction(override val schema: DBSchema) extends DBFunction[Unit, Unit, DBEngine](schema, "bar") { + override protected def query(values: Unit): dBEngine.QueryType[Unit] = neverHappens } val fnc1 = MyFunction(FooNamed) diff --git a/core/src/test/scala/za/co/absa/fadb/DBSchemaSuite.scala b/core/src/test/scala/za/co/absa/fadb/DBSchemaSuite.scala index cae060cd..85f7fc83 100644 --- a/core/src/test/scala/za/co/absa/fadb/DBSchemaSuite.scala +++ b/core/src/test/scala/za/co/absa/fadb/DBSchemaSuite.scala @@ -18,66 +18,28 @@ package za.co.absa.fadb import org.scalatest.funsuite.AnyFunSuite import za.co.absa.fadb.naming_conventions.SnakeCaseNaming.Implicits.namingConvention -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration.Duration import scala.concurrent.{Await, Future} class DBSchemaSuite extends AnyFunSuite { - private val awaitTime = Duration("1 sec") - private val executorThrow = new DBExecutor[String] { - override def run[R](fnc: QueryFunction[String, R]): Future[Seq[R]] = { + private object EngineThrow extends DBEngine { + override def run[R](query: QueryType[R]): Future[Seq[R]] = { throw new Exception("Should never get here") } } - private val executorResend = new DBExecutor[String] { - override def run[Int](fnc: QueryFunction[String, Int]): Future[Seq[Int]] = { - fnc("") - } - } - test("schema name default") { + class Foo extends DBSchema(EngineThrow) - class Foo(executor: DBExecutor[String]) extends DBSchema(executor) - val schema = new Foo(executorThrow) + val schema = new Foo assert(schema.schemaName == "foo") } test("schema name overridden") { - class Foo(executor: DBExecutor[String]) extends DBSchema(executor, Some("bar")) + class Foo extends DBSchema(EngineThrow, "bar") - val schema = new Foo(executorThrow) + val schema = new Foo assert(schema.schemaName == "bar") } - test("Test run call over") { - def queryFncSeq(s: String): Future[Seq[Int]] = { - Future { - Seq(1, 2, 3) - } - } - def queryFncEmpty(s: String): Future[Seq[Int]] = { - Future { - Seq.empty - } - } - - class Foo(executor: DBExecutor[String]) extends DBSchema(executor) - val schema = new Foo(executorResend) - - val resultExecuteSeq = Await.result(schema.execute(queryFncSeq), awaitTime) - assert(resultExecuteSeq == Seq(1, 2, 3)) - val resultUniqueSeq= Await.result(schema.unique(queryFncSeq), awaitTime) - assert(resultUniqueSeq == 1) - val resultOptionSeq = Await.result(schema.option(queryFncSeq), awaitTime) - assert(resultOptionSeq.contains(1)) - - val resultExecuteEmpty = Await.result(schema.execute(queryFncEmpty), awaitTime) - assert(resultExecuteEmpty.isEmpty) - val resultOptionEmpty = Await.result(schema.option(queryFncEmpty), awaitTime) - assert(resultOptionEmpty.isEmpty) - - } - } diff --git a/core/src/test/scala/za/co/absa/fadb/statushandling/StatusExceptionSuite.scala b/core/src/test/scala/za/co/absa/fadb/statushandling/StatusExceptionSuite.scala new file mode 100644 index 00000000..d6f8450b --- /dev/null +++ b/core/src/test/scala/za/co/absa/fadb/statushandling/StatusExceptionSuite.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2022ABSA 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.fadb.statushandling + +import org.scalatest.funsuite.AnyFunSuite +import za.co.absa.fadb.statushandling.StatusException._ + +class StatusExceptionSuite extends AnyFunSuite { + test("Test equals - when they are the same") { + val statusException = DataConflictException(FunctionStatus(10, "OK")) + val otherStatusException = DataConflictException(10, "OK") + + assert(statusException == otherStatusException) + } + + test("Test equals - when they are different") { + val statusException = DataNotFoundException(10, "OK") + val otherStatusException = DataNotFoundException(10, "Hello") + val anotherStatusException = DataNotFoundException(11, "OK") + + assert(statusException != otherStatusException) + assert(statusException != anotherStatusException) + } + + test("Test equals - when values are same but classes differ") { + val statusException = StatusException(10, "OK") + val otherStatusException = ServerMisconfigurationException(10, "OK") + + assert(statusException != otherStatusException) + } + + test("Test equals - when values are same but classes inheritance differ") { + val statusException = StatusException(10, "OK") + val otherException = new ClassNotFoundException() + + assert(statusException != otherException) + } +} diff --git a/core/src/test/scala/za/co/absa/fadb/statushandling/StatusHandlingSuite.scala b/core/src/test/scala/za/co/absa/fadb/statushandling/StatusHandlingSuite.scala new file mode 100644 index 00000000..fb65b759 --- /dev/null +++ b/core/src/test/scala/za/co/absa/fadb/statushandling/StatusHandlingSuite.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb.statushandling + +import org.scalatest.funsuite.AnyFunSuite +import za.co.absa.fadb.DBFunctionFabric +import za.co.absa.fadb.naming_conventions.{NamingConvention, SnakeCaseNaming} + +class StatusHandlingSuite extends AnyFunSuite { + test("Fields to select filled with default values") { + trait FooDBFunction extends DBFunctionFabric { + override def fieldsToSelect: Seq[String] = Seq("alpha", "beta") + } + + class StatusHandlingForTest extends FooDBFunction with StatusHandling { + override def functionName: String = "Never needed" + override def namingConvention: NamingConvention = SnakeCaseNaming.Implicits.namingConvention + + override protected def checkStatus(status: FunctionStatus) = throw new Exception("Should never get here") + override def fieldsToSelect: Seq[String] = super.fieldsToSelect + } + + val statusHandling = new StatusHandlingForTest + assert(statusHandling.fieldsToSelect == Seq("status", "status_text", "alpha", "beta")) + + } +} diff --git a/core/src/test/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandlingSuite.scala b/core/src/test/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandlingSuite.scala new file mode 100644 index 00000000..bde1ff4a --- /dev/null +++ b/core/src/test/scala/za/co/absa/fadb/statushandling/UserDefinedStatusHandlingSuite.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb.statushandling + +import org.scalatest.funsuite.AnyFunSuite +import za.co.absa.fadb.naming_conventions.{NamingConvention, SnakeCaseNaming} + +import scala.util.{Failure, Success, Try} + +class UserDefinedStatusHandlingSuite extends AnyFunSuite { + test("Check user defined status") { + class UserDefinedStatusHandlingForTest(val OKStatuses: Set[Integer]) extends UserDefinedStatusHandling { + override def checkStatus(status: FunctionStatus): Try[FunctionStatus] = super.checkStatus(status) + override def functionName: String = "Never needed" + override def namingConvention: NamingConvention = SnakeCaseNaming.Implicits.namingConvention + } + + val statusHandling = new UserDefinedStatusHandlingForTest(Set(200, 201)) + + val oK = FunctionStatus(200, "OK") + val alsoOK = FunctionStatus(201, "Also OK") + val notOK = FunctionStatus(500, "Not OK") + assert(statusHandling.checkStatus(oK) == Success(oK)) + assert(statusHandling.checkStatus(alsoOK) == Success(alsoOK)) + assert(statusHandling.checkStatus(notOK) == Failure(new StatusException(notOK))) + } +} diff --git a/core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingTest.scala b/core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingSuite.scala similarity index 69% rename from core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingTest.scala rename to core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingSuite.scala index ccc4a94f..004f7ed8 100644 --- a/core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingTest.scala +++ b/core/src/test/scala/za/co/absa/fadb/statushandling/fadbstandard/StandardStatusHandlingSuite.scala @@ -17,30 +17,34 @@ package za.co.absa.fadb.statushandling.fadbstandard import org.scalatest.funsuite.AnyFunSuite -import za.co.absa.fadb.statushandling.StatusException +import za.co.absa.fadb.exceptions.DBFailException +import za.co.absa.fadb.naming_conventions.{NamingConvention, SnakeCaseNaming} +import za.co.absa.fadb.statushandling.{FunctionStatus, StatusException} import za.co.absa.fadb.statushandling.StatusException._ import scala.reflect.ClassTag -import scala.util.Try +import scala.util.{Failure, Success, Try} -class StandardStatusHandlingTest extends AnyFunSuite { +class StandardStatusHandlingSuite extends AnyFunSuite { test("Verify checkStatus error mapping") { class StandardStatusHandlingForTest extends StandardStatusHandling { - override def checkStatus(status: Integer, statusText: String): Try[Unit] = super.checkStatus(status, statusText) + override def checkStatus(status: FunctionStatus): Try[FunctionStatus] = super.checkStatus(status) + override def checkStatus(status: Integer, statusText: String): Try[FunctionStatus] = super.checkStatus(status, statusText) override def functionName: String = "Never needed" + override def namingConvention: NamingConvention = SnakeCaseNaming.Implicits.namingConvention } def assertCheckStatusFailure[F <: StatusException](status: Int, statusText: String) (implicit classTag: ClassTag[F], checker: StandardStatusHandlingForTest): Unit = { + val failure = intercept[F] { checker.checkStatus(status, statusText).get } - assert(failure.status == status) - assert(failure.statusText == statusText) + assert(failure.status == FunctionStatus(status, statusText)) } implicit val standardStatusHandling: StandardStatusHandlingForTest = new StandardStatusHandlingForTest - assert(standardStatusHandling.checkStatus(10, "OK").isSuccess) + assert(standardStatusHandling.checkStatus(FunctionStatus(10, "OK")) == Success(FunctionStatus(10, "OK"))) assertCheckStatusFailure[ServerMisconfigurationException](21, "Server is wrongly set up") assertCheckStatusFailure[DataConflictException](31, "Referenced data does not allow execution of the request") assertCheckStatusFailure[DataNotFoundException](42, "Detail record not found") @@ -49,6 +53,10 @@ class StandardStatusHandlingTest extends AnyFunSuite { assertCheckStatusFailure[ErrorInDataException](73, "Value ABC is out of range") assertCheckStatusFailure[ErrorInDataException](84, "Json value of field FF is missing property PPP") assertCheckStatusFailure[OtherStatusException](95, "This is a special error") - assert(standardStatusHandling.checkStatus(101, "Server is wrongly set up").isFailure) + + val status = 101 + val statusText = "Server is wrongly set up" + val expectedFailure = Failure(DBFailException(s"Status out of range - with status: $status and status text: '${statusText}'")) + assert(standardStatusHandling.checkStatus(101, "Server is wrongly set up") == expectedFailure) } } diff --git a/examples/src/main/scala/za/co/absa/fadb/examples/enceladus/DatasetSchema.scala b/examples/src/main/scala/za/co/absa/fadb/examples/enceladus/DatasetSchema.scala index 3ecb9703..266c177b 100644 --- a/examples/src/main/scala/za/co/absa/fadb/examples/enceladus/DatasetSchema.scala +++ b/examples/src/main/scala/za/co/absa/fadb/examples/enceladus/DatasetSchema.scala @@ -17,28 +17,27 @@ package za.co.absa.fadb.examples.enceladus import za.co.absa.fadb.DBSchema -import za.co.absa.fadb.slick.{SlickPgExecutor, SlickPgFunction} +import za.co.absa.fadb.slick.{SlickPgEngine, SlickPgFunction, SlickPgFunctionWithStatusSupport} import za.co.absa.fadb.naming_conventions.SnakeCaseNaming.Implicits.namingConvention import slick.jdbc.{GetResult, SQLActionBuilder} -import za.co.absa.fadb.DBFunction._ import slick.jdbc.PostgresProfile.api._ import java.sql.Timestamp import scala.concurrent.Future import DatasetSchema._ -import za.co.absa.fadb.statushandling.StatusException +import za.co.absa.fadb.DBFunction.{DBSeqFunction, DBUniqueFunction} +import za.co.absa.fadb.statushandling.UserDefinedStatusHandling -class DatasetSchema(executor: SlickPgExecutor) extends DBSchema(executor) { +/* The Schema doesn't need the dBEngine directly, but it seems cleaner this way to hand it over to schema's functions */ +class DatasetSchema(implicit engine: SlickPgEngine) extends DBSchema { - private implicit val schema: DBSchema[ExecutorEngineType] = this val addSchema = new AddSchema val getSchema = new GetSchema - val listSchemas = new ListSchemas + val list = new List } object DatasetSchema { - type ExecutorEngineType = Database case class SchemaInput(schemaName: String, schemaVersion: Int, @@ -60,72 +59,64 @@ object DatasetSchema { deletedBy: Option[String], deletedWhen: Option[Timestamp]) - case class SchemaHeader(schemaName: String, schemaLatestVersion: Int) + case class SchemaHeader(entityName: String, entityLatestVersion: Int) - private implicit val SchemaHeaderImplicit: GetResult[SchemaHeader] = GetResult(r => {SchemaHeader(r.<<, r.<<)}) - private implicit val GetSchemaImplicit: GetResult[Schema] = GetResult(r => { - val status: Int = r.<< - val statusText: String = r.<< - if (status != 200) { - throw new StatusException(status, statusText) - } - Schema(r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<) - }) - - final class AddSchema(implicit schema: DBSchema[ExecutorEngineType]) - extends DBUniqueFunction[ExecutorEngineType, SchemaInput, Long](schema) - with SlickPgFunction[SchemaInput, Long] { + final class AddSchema(implicit override val schema: DBSchema, override val dbEngine: SlickPgEngine) + extends DBUniqueFunction[SchemaInput, Long, SlickPgEngine] + with SlickPgFunctionWithStatusSupport[SchemaInput, Long] + with UserDefinedStatusHandling { - - override protected def sqlToCallFunction(values: SchemaInput): SQLActionBuilder = { + override protected def sql(values: SchemaInput): SQLActionBuilder = { sql"""SELECT A.status, A.status_text, A.id_schema_version - FROM #$functionName(${values.schemaName}, ${values.schemaVersion}, ${values.schemaDescription}, - ${values.fields}::JSONB, ${values.userName} - ) A;""" + FROM #$functionName(${values.schemaName}, ${values.schemaVersion}, ${values.schemaDescription}, + ${values.fields}::JSONB, ${values.userName} + ) A;""" } - override protected def resultConverter: GetResult[Long] = { - val gr:GetResult[Long] = GetResult(r => { - val status: Int = r.<< - val statusText: String = r.<< - if (status != 201) throw new StatusException(status, statusText) - r.<< - }) - gr - } + override protected def slickConverter: GetResult[Long] = GetResult.GetLong + + override def OKStatuses: Set[Integer] = Set(201) + } - final class GetSchema(implicit schema: DBSchema[ExecutorEngineType]) - extends DBUniqueFunction[ExecutorEngineType, (String, Option[Int]), Schema](schema) - with SlickPgFunction[(String, Option[Int]), Schema] { + final class GetSchema(implicit override val schema: DBSchema, override val dbEngine: SlickPgEngine) + extends DBUniqueFunction[(String, Option[Int]), Schema, SlickPgEngine] + with SlickPgFunctionWithStatusSupport[(String, Option[Int]), Schema] + with UserDefinedStatusHandling { + /* This is an example of how to deal with overloaded DB functions - see different input type: Long vs what's in the class type: (String, Option[Int]) */ def apply(id: Long): Future[Schema] = { val sql = sql"""SELECT A.* FROM #$functionName($id) A;""" - schema.unique(makeQueryFunction(sql)(resultConverter)) + val slickQuery = new dBEngine.QueryType(sql, slickConverter) + dBEngine.unique[Schema](slickQuery) } - override protected def sqlToCallFunction(values: (String, Option[Int])): SQLActionBuilder = { + override protected def sql(values: (String, Option[Int])): SQLActionBuilder = { sql"""SELECT A.* - FROM #$functionName(${values._1}, ${values._2}) A;""" + FROM #$functionName(${values._1}, ${values._2}) A;""" + } + + override protected val slickConverter: GetResult[Schema] = GetResult{r => + Schema(r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<, r.<<) } - override protected def resultConverter: GetResult[Schema] = DatasetSchema.GetSchemaImplicit + override val OKStatuses: Set[Integer] = Set(200) } - final class ListSchemas(implicit schema: DBSchema[ExecutorEngineType]) - extends DBSeqFunction[ExecutorEngineType, Boolean, SchemaHeader](schema) + final class List(implicit override val schema: DBSchema, override val dbEngine: SlickPgEngine) + extends DBSeqFunction[Boolean, SchemaHeader, SlickPgEngine]() with SlickPgFunction[Boolean, SchemaHeader] { override def apply(values: Boolean = false): Future[Seq[SchemaHeader]] = super.apply(values) - override protected def sqlToCallFunction(values: Boolean): SQLActionBuilder = { - sql"""SELECT A.schema_name, A.schema_latest_version - FROM #$functionName($values) as A;""" + override protected def sql(values: Boolean): SQLActionBuilder = { + sql"""SELECT A.entity_name, A.entity_latest_version + FROM #$functionName($values) as A;""" } - override protected def resultConverter: GetResult[SchemaHeader] = DatasetSchema.SchemaHeaderImplicit + override protected val slickConverter: GetResult[SchemaHeader] = GetResult(r => {SchemaHeader(r.<<, r.<<)}) } } diff --git a/examples/src/test/scala/za/co/absa/fadb/examples/enceladus/DatasetSchemaSuite.scala b/examples/src/test/scala/za/co/absa/fadb/examples/enceladus/DatasetSchemaSuite.scala index 905fc232..07f9cb76 100644 --- a/examples/src/test/scala/za/co/absa/fadb/examples/enceladus/DatasetSchemaSuite.scala +++ b/examples/src/test/scala/za/co/absa/fadb/examples/enceladus/DatasetSchemaSuite.scala @@ -18,10 +18,9 @@ package za.co.absa.fadb.examples.enceladus import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpec -import za.co.absa.fadb.slick.SlickPgExecutor import za.co.absa.fadb.examples.enceladus.DatasetSchema._ -import za.co.absa.fadb.exceptions.DBFailException import slick.jdbc.PostgresProfile.api._ +import za.co.absa.fadb.slick.SlickPgEngine import za.co.absa.fadb.statushandling.StatusException import scala.concurrent.Await @@ -29,52 +28,52 @@ import scala.concurrent.duration.Duration class DatasetSchemaSuite extends AnyWordSpec with Matchers { private val db = Database.forConfig("menasdb") - private val executor = new SlickPgExecutor(db) - private val schemas = new DatasetSchema(executor) + private implicit val dbEngine: SlickPgEngine = new SlickPgEngine(db) + private val schemas = new DatasetSchema private def checkException(exception: StatusException): Unit = { - println(s"Requested failed with: ${exception.status} - ${exception.statusText}") + println(s"Requested failed with: ${exception.status.statusCode} - ${exception.status.statusText}") } // test cases are set to be ignored now, as they are not idempotent and require other project's (Enceladus) data structures - "listSchemas" should { - "list the schemas" ignore { - val ls = schemas.listSchemas() + "listSchemas" ignore { + "list the schemas" should { + val ls = schemas.list() val result = Await.result(ls, Duration.Inf) result.foreach(println) } } - "getSchema" should { + "getSchema" ignore { "return the particular schema" when { - "given name and version" ignore { + "given name and version" should { val ls = schemas.getSchema(("aaa", Option(1))) val result = Await.result(ls, Duration.Inf) println(result) } - "given id" ignore { + "given id" should { val gs = schemas.getSchema(1000000000000051L) val result = Await.result(gs, Duration.Inf) println(result) } } "return the latest schema version" when { - "only the schema name is given" ignore { + "only the schema name is given" should { val ls = schemas.getSchema(("aaa", None)) val result = Await.result(ls, Duration.Inf) println(result) } } "fail" when { - "schema does not exist" ignore { + "schema does not exist" should { val exception = intercept[StatusException] { val gs = schemas.getSchema(("xxx", None)) Await.result(gs, Duration.Inf) } checkException(exception) } - "requested schema version does not exist" ignore { + "requested schema version does not exist" should { val exception = intercept[StatusException] { val gs = schemas.getSchema(("aaa", Some(1000))) Await.result(gs, Duration.Inf) @@ -84,8 +83,8 @@ class DatasetSchemaSuite extends AnyWordSpec with Matchers { } } - "addSchema" should { - "add a schema" ignore { + "addSchema" ignore { + "add a schema" should { val schemaInput = SchemaInput( schemaName = "bbe", schemaVersion = 1, @@ -97,7 +96,7 @@ class DatasetSchemaSuite extends AnyWordSpec with Matchers { println(result) } "fail" when { - "Schema already exists" ignore { + "Schema already exists" should { val schemaInput = SchemaInput( schemaName = "aaa", schemaVersion = 2, @@ -110,7 +109,7 @@ class DatasetSchemaSuite extends AnyWordSpec with Matchers { } checkException(exception) } - "Schema version wrong" ignore { + "Schema version wrong" should { val schemaInput = SchemaInput( schemaName = "aaa", schemaVersion = 1000, diff --git a/project/plugins.sbt b/project/plugins.sbt index aefd7b2f..eb4198cd 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -36,3 +36,5 @@ addSbtPlugin("org.ow2.asm" % "asm-commons" % ow2Version from ow2Url("asm-commons addSbtPlugin("org.ow2.asm" % "asm-tree" % ow2Version from ow2Url("asm-tree")) addSbtPlugin("za.co.absa.sbt" % "sbt-jacoco" % "3.4.1-absa.4" from "https://github.com/AbsaOSS/sbt-jacoco/releases/download/3.4.1-absa.3/sbt-jacoco-3.4.1-absa.3.jar") + +addSbtPlugin("com.thoughtworks.sbt-api-mappings" % "sbt-api-mappings" % "3.0.2") diff --git a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgEngine.scala b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgEngine.scala new file mode 100644 index 00000000..effa4f24 --- /dev/null +++ b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgEngine.scala @@ -0,0 +1,52 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb.slick + + +import za.co.absa.fadb.DBEngine + +import scala.concurrent.Future +import slick.jdbc.PostgresProfile.api._ + +import scala.language.higherKinds + +/** + * [[DBEngine]] based on the Slick library in the Postgres flavor + * @param db - the Slick database + */ +class SlickPgEngine(val db: Database) extends DBEngine { + + /** + * The type of Queries for Slick + * @tparam T - the return type of the query + */ + type QueryType[T] = SlickQuery[T] + + /** + * Execution using Slick + * @param query - the Slick query to execute + * @tparam R - return the of the query + * @return - sequence of the results of database query + */ + override protected def run[R](query: QueryType[R]): Future[Seq[R]] = { + // It can be expected that a GetResult will be passed into the run function as converter. + // Unfortunately it has to be recreated to be used by Slick + val slickAction = query.sql.as[R](query.getResult) + db.run(slickAction) + } + +} diff --git a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunction.scala b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunction.scala index c6fbaa0f..401a4da7 100644 --- a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunction.scala +++ b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunction.scala @@ -17,24 +17,63 @@ package za.co.absa.fadb.slick import slick.jdbc.{GetResult, SQLActionBuilder} -import slick.jdbc.PostgresProfile.api._ -import za.co.absa.fadb.{DBFunctionFabric, QueryFunction} +import za.co.absa.fadb.DBFunctionFabric -trait SlickPgFunction[T, R] extends DBFunctionFabric { +/** + * Mix-in trait to use [[za.co.absa.fadb.DBFunction DBFunction]] with [[SlickPgEngine]]. Implements the abstract function `query` + * @tparam I - The input type of the function + * @tparam R - The return type of the function + */ +trait SlickPgFunction[I, R] extends DBFunctionFabric { - protected def sqlToCallFunction(values: T): SQLActionBuilder + /** + * A reference to the [[SlickPgEngine]] to use the [[za.co.absa.fadb.DBFunction DBFunction]] with + */ + implicit val dbEngine: SlickPgEngine - protected def resultConverter: GetResult[R] + /** + * This is expected to return SQL part of the [[SlickQuery]] (eventually returned by the `SlickPgFunction.query` function + * @param values - the values to pass over to the database function + * @return - the Slick representation of the SQL + */ + protected def sql(values: I): SQLActionBuilder - protected def makeQueryFunction(sql: SQLActionBuilder)(implicit rconv: GetResult[R]): QueryFunction[Database, R] = { - val query = sql.as[R] - val resultFnc = {db: Database => db.run(query)} - resultFnc + /** + * This is expected to return a method to convert the [[slick.jdbc.PositionedResult slick.PositionedResult]], the Slick general SQL result + * format into the `R` type + * @return - the converting function + */ + protected def slickConverter: GetResult[R] + + /** + * Alias to use within the SQL query + */ + protected val alias = "FNC" + + /** + * Helper function to use in the actual DB function class + * @return the SELECT part of the function call SQL query + */ + protected def selectEntry: String = { // TODO Not suggested to use until #6 will be implemented + val fieldsSeq = fieldsToSelect + if (fieldsSeq.isEmpty) { + "*" + } else { + val aliasToUse = if (alias.isEmpty) { + "" + } else { + s"$alias." + } + fieldsToSelect.map(aliasToUse + _).mkString(",") + } } - protected def queryFunction(values: T): QueryFunction[Database, R] = { - val converter = resultConverter - val functionSql = sqlToCallFunction(values) - makeQueryFunction(functionSql)(converter) + /** + * This mix-in main reason of existence. It implements the `query` function for [[za.co.absa.fadb.DBFunction DBFunction]] for [[SlickPgEngine]] + * @param values - the values to pass over to the database function + * @return - the SQL query in [[SlickQuery]] form + */ + protected def query(values: I): dbEngine.QueryType[R] = { + new SlickQuery(sql(values), slickConverter) } } diff --git a/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunctionWithStatusSupport.scala b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunctionWithStatusSupport.scala new file mode 100644 index 00000000..e1588372 --- /dev/null +++ b/slick/src/main/scala/za/co/absa/fadb/slick/SlickPgFunctionWithStatusSupport.scala @@ -0,0 +1,64 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb.slick + +import slick.jdbc.{GetResult, PositionedResult} +import za.co.absa.fadb.statushandling.FunctionStatus + +import scala.util.Try + +/** + * An extension of the [[SlickPgFunction]] mix-in trait to add support of status handling + * This trait expects another mix-in of [[za.co.absa.fadb.statushandling.StatusHandling StatusHandling]] (or implementation of `checkStatus` function) + * @tparam I - The input type of the function + * @tparam R - The return type of the function + */ +trait SlickPgFunctionWithStatusSupport[I, R] extends SlickPgFunction[I, R] { + + /** + * Function which should actually check the status code returned by the DB function. Expected to got implemented by + * [[za.co.absa.fadb.statushandling.StatusHandling StatusHandling]] successor trait. But of course can be implemented directly. + * @param status - the status to check + * @return - Success or failure the status means + */ + protected def checkStatus(status: FunctionStatus): Try[FunctionStatus] + + /** + * A special extension of the converting function that first picks up status code and status check and checks for their + * meaning. Then the original conversion is executed. + * @param queryResult - the result of the SQL query, the input of the original converting function + * @param actualConverter - the original converting function + * @return - new converting function that also checks for status + */ + private def converterWithStatus(queryResult: PositionedResult, actualConverter: GetResult[R]): R = { + val status:Int = queryResult.<< + val statusText: String = queryResult.<< + checkStatus(FunctionStatus(status, statusText)).get //throw exception if status was off + actualConverter(queryResult) + } + + /** + * Replaces the converter with one that also extracts and checks status code and status text. + * @param values - the values to pass over to the database function + * @return - the SQL query in [[SlickQuery]] form + */ + override protected def query(values: I): dbEngine.QueryType[R] = { + val original = super.query(values) + new SlickQuery[R](original.sql, GetResult{converterWithStatus(_, original.getResult)}) + } + +} diff --git a/slick/src/main/scala/za/co/absa/fadb/slick/SlickQuery.scala b/slick/src/main/scala/za/co/absa/fadb/slick/SlickQuery.scala new file mode 100644 index 00000000..8cc6335a --- /dev/null +++ b/slick/src/main/scala/za/co/absa/fadb/slick/SlickQuery.scala @@ -0,0 +1,29 @@ +/* + * Copyright 2022 ABSA Group Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package za.co.absa.fadb.slick + +import slick.jdbc.{GetResult, SQLActionBuilder} +import za.co.absa.fadb.Query + +/** + * SQL query representation for Slick + * @param sql - the SQL query in Slick format + * @param getResult - the converting function, that converts the [[slick.jdbc.PositionedResult slick.PositionedResult]] (the result of Slick + * execution) into the desire `R` type + * @tparam R - the return type of the query + */ +class SlickQuery[R](val sql: SQLActionBuilder, val getResult: GetResult[R]) extends Query[R]