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

#35: Refactor the core for more logical structure #36

Merged
Merged
Show file tree
Hide file tree
Changes from 14 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
71 changes: 71 additions & 0 deletions core/src/main/scala/za/co/absa/fadb/DBEngine.scala
Original file line number Diff line number Diff line change
@@ -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 X - the return type of the query
*/
type QueryType[X] <: Query[X]

/**
* 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] = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking about whether to name this as executeOne

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like 'execute' as prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I on the other hand like them distinguished. 😉 Easier to read. and also nicely binds to the successor classes.

Copy link
Collaborator

@lsulak lsulak Jun 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think option is easier to read. It's not intuitive. We are actually executing a SQL query here.

In regards to the unique ... it's kind of better, but still, I don't really like it that much.

Let's have an inspiration from some most famous DB libraries out there. For example SQLAlchemy. There are functions like fetchOne, executeMany etc - https://peps.python.org/pep-0249/#executemany and I think that's much better, because it not only says what the result kind of should be, but also it says about what is actually happening. Does it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, Will think about the names 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed to do the renames as part of #34

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay

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)
}
}

185 changes: 153 additions & 32 deletions core/src/main/scala/za/co/absa/fadb/DBFunction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
* @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 T - 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[T, 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: T): 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) {
Expand All @@ -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: T): Future[Seq[R]] = dBEngine.execute[R](query(values))
protected def unique(values: T): Future[R] = dBEngine.unique(query(values))
protected def option(values: T): 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
* @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 T - 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[T, R, E <: DBEngine](functionNameOverride: Option[String] = None)
(implicit schema: DBSchema, dBEngine: E)
extends DBFunction[T, 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: T): 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
* @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 T - 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[T, R, E <: DBEngine](functionNameOverride: Option[String] = None)
(implicit schema: DBSchema, dBEngine: E)
extends DBFunction[T, 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: T): 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
* @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 T - 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[T, R, E <: DBEngine](functionNameOverride: Option[String] = None)
(implicit schema: DBSchema, dBEngine: E)
extends DBFunction[T, 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: T): Future[Option[R]] = option(values)
}
}
8 changes: 8 additions & 0 deletions core/src/main/scala/za/co/absa/fadb/DBFunctionFabric.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading