Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#5: Traits to support status_code reaction #32

Merged
merged 7 commits into from
May 31, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/src/main/scala/za/co/absa/fadb/DBFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import scala.concurrent.Future
* @tparam T - the type covering the input fields of the database function
* @tparam R - the type covering the returned fields from the database function
*/
abstract class DBFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) {
abstract class DBFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Option[String] = None) extends DBFunctionFabric {
val functionName: String = {
val fn = functionNameOverride.getOrElse(schema.objectNameFromClassName(getClass))
if (schema.schemaName.isEmpty) {
Expand All @@ -39,6 +39,13 @@ abstract class DBFunction[E, T, R](schema: DBSchema[E], functionNameOverride: Op
}
}

/**
* 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
*/
protected def queryFunction(values: T): QueryFunction[E, R]
}

Expand Down
27 changes: 27 additions & 0 deletions core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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

/**
* This trait serves the purpose of introducing functions that are common to all DB Function objects and mix-in traits
* that offer certain implementations. This trait should help with the inheritance of all of these
*/
trait DBFunctionFabric {
def functionName: String

protected def fieldsToSelect: Seq[String] = Seq.empty
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,12 @@

package za.co.absa.fadb.exceptions

case class DBFailException(status: Int, message: String) extends Exception(message)
/**
* General Fa-DB exception class
* @param message - the message describing the reason of exception
*/
class DBFailException(message: String) extends Exception(message)

object DBFailException {
def apply(message: String): DBFailException = new DBFailException(message)
}
6 changes: 6 additions & 0 deletions core/src/main/scala/za/co/absa/fadb/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,11 @@ package za.co.absa
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]]
}
Original file line number Diff line number Diff line change
@@ -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 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
*/
class StatusException(val status: Int, statusText: String) extends DBFailException(statusText) {
def statusText: String = getMessage
}

object StatusException {
class ServerMisconfigurationException(status: Int, statusText: String) extends StatusException(status, statusText)

class DataConflictException(status: Int, statusText: String) extends StatusException(status, statusText)

class DataNotFoundException(status: Int, statusText: String) extends StatusException(status, statusText)

class ErrorInDataException(status: Int, statusText: String) extends StatusException(status, statusText)

class OtherStatusException(status: Int, statusText: String) extends StatusException(status, statusText)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 za.co.absa.fadb.DBFunctionFabric
import za.co.absa.fadb.statushandling.StatusHandling.{defaultStatusFieldName, defaultStatusTextFieldName}

import scala.util.Try

/**
* A basis for mix-in traits for [[DBFunction]] that support `status` and `status_text` for easier handling
*/
trait StatusHandling extends DBFunctionFabric{

def statusFieldName: String = defaultStatusFieldName
def statusTextFieldName: String = defaultStatusTextFieldName

override protected def fieldsToSelect: Seq[String] = {
Seq(statusFieldName, statusTextFieldName) ++ super.fieldsToSelect
}

protected def checkStatus(status: Integer, statusTex: String): Try[Unit]
}

object StatusHandling {
val defaultStatusFieldName = "status"
val defaultStatusTextFieldName = "status_test"
benedeki marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Intro

This is a list of convention status codes to return from functions - the primary usage intention is in Postgres DB
functions, particularly in combination with fa-db library.

The expected usage is that each function returns at least two values - `status` and `status_text`.

Status is one of the values below. Status text is a human readable explanation of what happened. Status text should not
have a syntactical meaning.

Exceptions from the rule above:
* Immutable functions returning simple value
* Function returning a (large) set of values, basically a query, where the obvious result would be no records for _“not found”_, otherwise a general ok `status` and `status_text` on each line, just increasing the size of returned data.
* _“Private”_ functions (not callable from outside of DB (no grants), name starting with underscore), if it’s more convenient for the usage of the function. It’s still recommended to use the `status` and `status_text` even there.

General principle of status (code):
* The codes are two digit
* They are divided into sections
* There are fixed values with a preferred meaning, try to keep to it
* In each range there are “open” values to use in other or sub-cases of the general range meaning. Their meaning is specific the each function and depends on the contract between the function and the app calling it
* Function should list all the possible returned statuses in it’s header/inline documentation together with their used meaning
* When a status code suggests a not-OK state (values 20-99) the eventual other returned field values beyond `status` and `status_text` are undefined (probably will be NULL, but it should not be taken as a fact)
* The possible returned status codes and their meaning should be described in the function comments

# Codes

The codes to be double digit

## OK/Correct outcome

### 10-19

| 10 | general ok |
| 11 | data created |
| 12 | data updated |
| 14 | no op needed |
| 15 | data deleted |

Rest unspecified OK status, to be agreed in contract between DB and app

## Server misconfiguration

### 20-29

Specific to the database application, should be shared over all functions in that DB.

## Data conflict

### 30-39

| 30 | general data conflict |
| 31 | referenced data does not allow execution of the request |

Rest of the codes meaning depends on agreement in contact between DB and app

## Data not found

### 40-49

| 40 | requested data not found |
| 41 | master record not found |
| 42 | detail record not found |

Rest of the codes meaning depends on agreement in contact between DB and app

## Error in data

### 50-89

| 50-59 | generally incorrect data (when obvious from context) |
| 60-69 | missing value (usually NULL) |
| 70-79 | value out of range |
| 80-89 | wrong content of complex types (json, xml, hstore), like missing key, undesired key etc. |

## Free range for other errors

### 90-99

Rest of the codes meaning depends on agreement in contact between DB and app
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.fadbstandard

import za.co.absa.fadb.exceptions.DBFailException
import za.co.absa.fadb.statushandling.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.
*/
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'"))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Copyright 2022 ABSA Group Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package za.co.absa.fadb.statushandling.fadbstandard

import org.scalatest.funsuite.AnyFunSuite
import za.co.absa.fadb.statushandling.StatusException
import za.co.absa.fadb.statushandling.StatusException._

import scala.reflect.ClassTag
import scala.util.Try

class StandardStatusHandlingTest 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 functionName: String = "Never needed"
}

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)
}
implicit val standardStatusHandling: StandardStatusHandlingForTest = new StandardStatusHandlingForTest

assert(standardStatusHandling.checkStatus(10, "OK").isSuccess)
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")
assertCheckStatusFailure[ErrorInDataException](58, "Some incorrect data")
assertCheckStatusFailure[ErrorInDataException](69, "Missing value for field XYZ")
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)
}
}
Loading