Package: easyquery-query-builder
easyquery-query-builderMain purpose of package is to provide user with ability to create sql queries using QueryBuilders which are implemented in Builder Project Pattern.InstallationUsing pip:pipinstalleasyquery-query-builderUsing poetry:poetryaddeasyquery-query-builderUsing pipenv:pipenvinstalleasyquery-query-builderTests and coverageAll functionalities are fully tested.You are able to run tests on your own by using cloning repository and using it's virtual environment.1. Clone repository and enter it's main directorygitclonehttps://github.com/DSmolke/EASYQUERY_QUERY_BUILDER.gitcdEASYQUERY_QUERY_BUILDER2. Run tests using:pip:pipinstallpytestpipinstallcoveragepipinstalleasyvalid-data-validatorpytest-vvpoetry:pipinstallpoetrypoetryupdatepoetryshellpoetryrunpytest-vv(poetry update is used due to some issue that I faced with newest Python version)pipenv:pipinstallpipenvpipenvshellpipenvrunpytest-vv3. Access coverage reportIf you haven't done step 1, start from here:gitclonehttps://github.com/DSmolke/EASYQUERY_QUERY_BUILDER.gitcdEASYQUERY_QUERY_BUILDERThen use this command to change branchgitcheckoutwith_coverage_filesYou should be able to see htmlcov directory. Enter it and open index.html file to see full coverage report.Basic usageLet's say we need to prepare query where we ask database for all records from drivers table, joined with licenses table on license id:from easyquery_query_builder.queries.read_query_with_joins import ReadQueryWithJoins
from easyquery_query_builder.queries.read_query_with_joins_builder import ReadQueryWithJoinsBuilder

query: ReadQueryWithJoins = ReadQueryWithJoinBuilder() \
    .add_select_statement('*') \
    .add_from_statement('drivers') \
    .add_joins_statement([['licenses', 'l', 'l.id = drivers.license_id']])\
    .build()Once we built our query it's ready to be used:print(query.parse())with result of:select * from drivers join licenses as l on l.id = drivers.license_idObjectsAt this stage of development one abstract class and two
inheriting classes are implemented:QueryContains one abstract method .parse() which force to take no arguments and return string value.
It will be used for final step in query creation.class Query(ABC):
    
    @abstractmethod
    def parse(self) -> str:
        passReadQueryIt's purpose is to create and manage sql query that will be returned using parse method.Can be initialised without arguments or with one or more of them:class ReadQuery:
    def __init__(self, select_="", from_="", where_="", group_by_="", having_="", order_by_=""):
        """ Empty query is created if no values are provided.(designed for builder)"""
        self.select_ = select_
        self.from_ = from_
        self.where_ = where_
        self.group_by_ = group_by_
        self.having_ = having_
        self.order_by_ = order_by_As you can see, all statements like select, from etc. have sufix of '_'. It helps with modern IDE like PyCharm to hide sql syntax which can be targeted and indicate fake errors occurring in the code.Parse method after simple arguments validation and query logic validation creates sql query expression:def parse(self) -> str:
        """ Creates sql query expression using fields provided in instance """

        # validation of all fields - checks only types
        query_data = self.__dict__
        constraints = {
            "select_": {Constraint.IS_TYPE: str},
            "from_": {Constraint.IS_TYPE: str},
            "where_": {Constraint.IS_TYPE: str},
            "group_by_": {Constraint.IS_TYPE: str},
            "having_": {Constraint.IS_TYPE: str},
            "order_by_": {Constraint.IS_TYPE: str}
        }
        validate_json_data(query_data, constraints)

        # validation that force all fields to be properly structured - minimum of select and from statements, having only with group by
        s, f, w, g, h = self.select_, self.from_, self.where_, self.group_by_, self.having_

        if (s == "" and f == "") or (s != "" and f == "") or (s == "" and f != ""):
            raise ValueError("Query requirement is to have select and from statements")
        if h != "" and g == "":
            raise ValueError("You cannot use having block without declaring group by block")

        # creation of sql query
        statement = f"{f'select {self.select_}' if self.select_ else ''}" \
                    f"{f' from {self.from_}' if self.from_ else ''}" \
                    f"{f' where {self.where_}' if self.where_ else ''}" \
                    f"{f' group by {self.group_by_}' if self.group_by_ else ''}" \
                    f"{f' having {self.having_}' if self.having_ else ''}" \
                    f"{f' order by {self.order_by_}' if self.order_by_ else ''}"
        return statementReadQueryWithJoinsClass that inheriting ReadQuery, there are two minor differences between them:Initialisation:class ReadQueryWithJoins(ReadQuery):
    """ Subclass of ReadQuery which implements joins """
    def __init__(self, select_="", from_="", where_="", group_by_="", having_="", order_by_="", joins_=""):
        super().__init__(select_, from_, where_, group_by_, having_, order_by_)
        self.joins_: list[list[str] | str] = joins_filed joins_ addedParse methoddef parse(self) -> str:
        """ Creates sql query expression can be extended with join statements using fields provided in instance """

        # validation of all fields - checks only types and types of members
        query_data = self.__dict__
        constraints = {
            "select_": {Constraint.IS_TYPE: str},
            "from_": {Constraint.IS_TYPE: str},
            "where_": {Constraint.IS_TYPE: str},
            "group_by_": {Constraint.IS_TYPE: str},
            "having_": {Constraint.IS_TYPE: str},
            "order_by_": {Constraint.IS_TYPE: str},
            "joins_": {Constraint.IS_TYPE: list, Constraint.ARRAY_MEMBERS_TYPE: list}
        }
        validate_json_data(query_data, constraints)

        # validation that force all fields to be properly structured - minimum of select and from statements, having only with group by
        s, f, w, g, h = self.select_, self.from_, self.where_, self.group_by_, self.having_
        if (s == "" and f == "") or (s != "" and f == "") or (s == "" and f != ""):
            raise ValueError("Query requirement is to have select and from statements")

        if h != "" and g == "":
            raise ValueError("You cannot use having block without declaring group by block")

        # concatenation of joins
        joins_exp = " ".join([f"join {table} as {alias} on {conditions}" for table, alias, conditions in self.joins_])

        # creation of sql query
        statement = f"{f'select {self.select_}' if self.select_ else ''}" \
                    f"{f' from {self.from_}' if self.from_ else ''}" \
                    f" {joins_exp}" \
                    f"{f' where {self.where_}' if self.where_ else ''}" \
                    f"{f' group by {self.group_by_}' if self.group_by_ else ''}" \
                    f"{f' having {self.having_}' if self.having_ else ''}" \
                    f"{f' order by' if self.order_by_ else ''}"
        return statement"joins_": {Constraint.IS_TYPE: list, Constraint.ARRAY_MEMBERS_TYPE: list}Added to constraints and validated, as well as jons_exp now is a part of sql statement.BuildersTheir role is to create new or manage existing queriesReadQueryBuilderInitialisation:class ReadQueryBuilder:
    """ Builder used to create new ReadQueries 'from scratch' or modify existing ones to desired form """
    def __init__(self, query: Query | None = None):
        if query is None:
            self.query = ReadQuery()
        else:
            self.query = queryAt this stage, new ReadQuery is made if no query is provided as an argument.Query management:User can change state of query at any point by using one of method that has same structure as this one:def add_select_statement(self, new_select: str) -> Self:
        """ Ads new select statement provided by user. Basic validation of argument is performed"""
        data = {"new_select": new_select}
        constraints = {"new_select": {Constraint.IS_TYPE: str}}
        validate_json_data(data, constraints)
        self.query.select_ = new_select
        return selfIt allows us to chain methods:ReadQueryBuilder().add_select_statement('*').add_from_statement('cars').add_where_statement('production_year > 2020')List of valid methods:add_select_statementadd_from_statementadd_where_statementadd_group_by_statementadd_having_statementadd_order_by_statementandbuild methodBasically returns query that was created or managed by builder:def build(self) -> ReadQuery:
        """ Builds query based on all operations that were made """
        return self.queryReadQueryWithJoinsBuilderInitialisation:Inherits ReadQueryBuilder but operates on ReadQueryWithJoins:def __init__(self, query: Query | None = None):
        if query is None:
            self.query = ReadQueryWithJoins()
        else:
            self.query = queryQuery management:It has all the futures of ReadQueryBuilder but adds method for joins:def add_joins_statement(self, new_joins: str) -> Self:
        """ Ads new joins arguments provided by user. Basic validation of argument is performed"""
        data = {"new_joins": new_joins}
        constraints = {"new_joins": {Constraint.IS_TYPE: list, Constraint.ARRAY_MEMBERS_TYPE: list}}
        validate_json_data(data, constraints)
        self.query.joins_ = new_joins
        return selfList of valid methods:add_select_statementadd_from_statementadd_where_statementadd_group_by_statementadd_having_statementadd_order_by_statementadd_joins_statementandbuild methodBasically returns query that was created or managed by builder:def build(self) -> ReadQueryWithJoins:
        """ Builds query based on all operations that were made """
        return self.queryRules and ErrorsWhen using all add_..._statement methods accept add_joins_statement, user needs to give expression with awareness of sql syntax:ReadQueryBuilder().add_select_statement('id, age')
ReadQueryBuilder().add_select_statement('*')
ReadQueryBuilder().add_select_statement('cars.id, owner.*')Mandatory statements are select and from, if rule won't be followed it will cause ValueError:query = ReadQueryBuilder().add_select_statement('*').build()
query.parse()File "<some path....>", line 47, in parse
    raise ValueError("Query requirement is to have select and from statements")
ValueError: Query requirement is to have select and from statementsSame thing will happen if we'll use having statement without group_by statementquery = ReadQueryBuilder()\
    .add_select_statement('*')\
    .add_from_statement('cars')\
    .add_having_statement("value > 300000")\
    .build()
query.parse()File "<some path....>", line 49, in parse
    raise ValueError("You cannot use having block without declaring group by block")
ValueError: You cannot use having block without declaring group by blockWhen we use ReadQueryWithJoins we need to provide list of lists that contains table name, alias, and condition of join. We can use multiple joins:query = ReadQueryWithJoinsBuilder()\
    .add_select_statement('*')\
    .add_from_statement('cars')\
    .add_joins_statement([['drivers', 'd', 'd.id = cars.driver_id'], ['producers', 'p', 'p.id = cars.prod_id']])\
    .build()
query.parse()Outcome:select * from cars join drivers as d on d.id = cars.driver_id join producers as p on p.id = cars.prod_idAny attempt of providing argument with wrong type will cause error in add_..._statement as well as in parse method:query = ReadQueryWithJoinsBuilder()\
    .add_select_statement(100)\
    .add_from_statement('cars')query = ReadQueryWithJoinsBuilder().add_select_statement(100)File "<some path....>", line 121, in validate_json_data
    raise ValidationError(dict(errors_by_key))
easyvalid_data_validator.customexceptions.common.ValidationError: {'new_select': ["Invalid type - isn't same type like compare type"]}or in parse:sql_expression = ReadQuery(select_=100).parse()File File "<some path....>", line 121, in validate_json_data
    raise ValidationError(dict(errors_by_key))
easyvalid_data_validator.customexceptions.common.ValidationError: {'select_': ["Invalid type - isn't same type like compare type"]}Documentation linkGithub