diff --git a/README.md b/README.md index 3bde08d..ea08d8a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,410 @@ -# postgres-dynamic -Postgres Dynamic Query +# postgres-dynamic - Python-PostgreSQL Dynamic Query Builder + +Postgres dynamic is a simple query builder developed for internal usage. It currently supports select, insert, update, and delete statements. +The purpose of this library is for better managament and maintenance of the code used in our environment. + +## Parameter Format: +- connection_string: dict + ``` + connection_string = { + 'PG_HOST': 'YOUR_CONNETION_HOST_ADDRESS', + 'PG_PORT': 'YOUR_CONNECTION_PORT', + 'PG_DATABASE': 'YOUR_CONNECTION_DATABASE_NAME', + 'PG_USER': 'YOUR_CONNECTION_USERNAME', + 'PG_PASSWORD': 'YOUR_CONNECTION_PASSWORD', + } + ``` +- connection_object: Callable + ``` + connection_object = psycopg2.connect(host,port,database,user,password) #object created from psycopg2.connect() + ``` +- where: List(dict) + ``` + where = [ + { + 'column_name': 'some_column_name', + 'value': 'some_value', # can accept str, int, list, or tuple + 'operator': 'some_operator', # can be omitted (accepted operators are =, >, <, >=, <=, IN), + 'conjunction': 'some_conjunction', # can be omitted, used when you need to specify more than one conditions and will link with next index value (accepted conjunctions are AND, OR) + }, + ], + ``` + +- main_table: Union[dict, str] + + For **select** query + ``` + main_table = { + 'table': 'some_table_name', + 'alias': 'some_alias_for_table', + }, + ``` + + For **transaction** query + ``` + main_table = 'some_table_name' + ``` + +- join_table: List(dict) + ``` + join_table = [ + { + 'table': 'some_table_name', + 'alias': 'some_alias_for_table', + 'join_method': 'join_method', # accepted join methods are (INNER, LEFT, RIGHT, FULL) + 'on': 'matching_column_on_both_table', + }, + ] + ``` + +- column_name: List(str) + ``` + column_name = ['some_column_name', 'some_column_name', 'some_column_name',] + ``` + +- column_and_value: dict + ``` + column_and_value = { + 'some_column_name': 'some_value', # for multiple values just provide more key:value pair + } + ``` + +- order: dict + ``` + order = { + 'some_column_name': 'ASC', # accepted order values are (ASC, DESC), for multiple order conditions just provide more key:value pair + } + ``` + +## Usage & Code Samples + +Example DB + +`table:` **employees** +| id | first_name | last_name +| --- | --- | --- +| 1 | Alex | Garcia +| 2 | Joe | Black +| 3 | John | Doe +| 4 | Barry | Allen +| 5 | Charlie | Cox + +`table:` **salaries** +| employee_id | salary +| --- | --- +| 1 | 120,000 +| 2 | 135,000 +| 3 | 150,000 +| 4 | 180,000 +| 5 | 120,000 + +- SELECT + + - Single Select + Single select always return a single value from the query, based on `fetchone` in psycopg2 and returning a dictionary with `{column_name: value}` of the tables. +
+ Show more... + + Parameters: + ``` + connection_string #required + main_table #required + where #required + join_table #optional (if omitted it won't join to any table) + column_name #optional (if omitted it will select all columns on the provided table) + ``` + + Code samples: + ``` + # without joining table + + from postgres_dynamic import PGDGet + import asyncio + + query_result = PGDGet.get_one( + connection_string={ + 'PG_HOST': 'localhost', + 'PG_PORT': 5432, + 'PG_DATABASE': 'postgres', + 'PG_USER': 'postgres', + 'PG_PASSWORD': 'password' + }, + main_table={'table': 'employees'}, + where=[ + {'column_name': 'id', 'value': '1'}, + ], + column_name=['first_name'] + ) + + result = asyncio.run(query_result) + print(result) + + # {'first_name': 'Alex'} + ``` + + ``` + # with join table salaries + + query_result = PGDGet.get_one( + connection_string={ + 'PG_HOST': 'localhost', + 'PG_PORT': 5432, + 'PG_DATABASE': 'postgres', + 'PG_USER': 'postgres', + 'PG_PASSWORD': 'password' + }, + main_table={'table': 'employees', 'alias': 'emp'}, + join_table=[ + {'table': 'salaries', 'alias': 'sal', 'join_method': 'INNER', 'on': 'emp.id = sal.employee_id'} + ], + where=[ + {'column_name': 'id', 'value': '1'}, + ], + ) + + result = asyncio.run(query_result) + print(result) + + # {'id': '1', 'first_name': 'Alex', 'last_name': 'Garcia', 'employee_id': '1', 'salary': 120000} + ``` +
+ + - Multi Select + Multi select always return a dict with key `data`, based on `fetchall` in psycopg2 and returning a list of dictionary with `{column_name: value}` of the tables. + Parameters: +
+ Show more... + + ``` + connection_string #required + main_table #required + where #optional (if omitted no condition will be passed) + join_table #optional (if omitted it won't join to any table) + column_name #optional (if omitted it will select all columns on the provided table) + order #optional (if omitted it won't sort the query) + limit #optional (if a limit count is given, no more than that many rows will be returned but possibly fewer, if the query itself yields fewer rows) + offset #optional (it used to skip that many rows before beginning to return rows) + + notes: + - If both OFFSET and LIMIT appear, then OFFSET rows are skipped before starting to count the LIMIT rows that are returned + - When using LIMIT, it is important to use an ORDER BY clause that constrains the result rows into a unique order. Otherwise you will get an unpredictable subset of the query's rows. + - For paging, you can specify 0 or 1 for the starting point of the first page + ``` + + Code samples: + ``` + from postgres_dynamic import PGDGet + import asyncio + + query_result = PGDGet.get_all( + connection_string={ + 'PG_HOST': 'localhost', + 'PG_PORT': 5432, + 'PG_DATABASE': 'postgres', + 'PG_USER': 'postgres', + 'PG_PASSWORD': 'password' + }, + main_table={'table': 'employees'}, + limit=3, + offset=2 + ) + + result = asyncio.run(query_result) + print(result) + + # {'data': [{'id': '4', 'first_name': 'Barry', 'last_name': 'Allen'}, {'id': '5', 'first_name': 'Charlie', 'last_name': 'Cox'}]} + ``` +
+ + + +- INSERT + + - Insert Statement + Insert will not return anyting, and will not saved changes to the database unless you specify `commit=True` in the parameters. +
+ Show more... + + Parameters: + ``` + connection_object #required + main_table #required + column_and_value #required + commit #optional (if omitted, default value will be False which will not saving any changes to database) + ``` + + Code samples: + ``` + # with auto commit + + from postgres_dynamic import PGDTransaction + import asyncio + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.insert( + connection_object=connection_object, + main_table='employees', + column_and_value={'id': 6, 'first_name': 'Harrison', 'last_name': 'Ford'}, + commit=True + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will insert a new employee to the employees table + ``` + + ``` + # without auto commit + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.insert( + connection_object=connection_object, + main_table='salaries', + column_and_value={'employee_id': 6, 'salary': 250000}, + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will insert a new salary to the salaries table + + # save changes to the database + connection_object.commit() + + ``` + +
+ +- UPDATE + + - Update Statement + Update will not return anyting, and will not saved changes to the database unless you specify `commit=True` in the parameters. +
+ Show more... + + Parameters: + ``` + connection_object #required + main_table #required + column_and_value #required + where #required + commit #optional (if omitted, default value will be False which will not saving any changes to database) + ``` + + Code samples: + ``` + # with auto commit + + from postgres_dynamic import PGDTransaction + import asyncio + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.update( + connection_object=connection_object, + main_table='employees', + column_and_value={'first_name': 'Tyler', 'last_name': 'Oakley'}, + where=[ + {'column_name': 'id', 'value': '6'}, + ], + commit=True + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will update employee first_name and last_name with id 6 + ``` + + ``` + # without auto commit + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.update( + connection_object=connection_object, + main_table='salaries', + column_and_value={'salary': 450000}, + where=[ + {'column_name': 'employee_id', 'value': '6'}, + ], + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will update the salary with employee_id 6 + + # save changes to the database + connection_object.commit() + + ``` + +
+ + +- DELETE + + - Delete Statement + Delete will not return anyting, and will not saved changes to the database unless you specify `commit=True` in the parameters. +
+ Show more... + + Parameters: + ``` + connection_object #required + main_table #required + where #required + commit #optional (if omitted, default value will be False which will not saving any changes to database) + ``` + + Code samples: + ``` + # with auto commit + + from postgres_dynamic import PGDTransaction + import asyncio + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.delete( + connection_object=connection_object, + main_table='salaries', + where=[ + {'column_name': 'employee_id', 'value': '6'}, + ], + commit=True + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will delete salary data with employee_id 6 + ``` + + ``` + # without auto commit + + connection_object = psycopg2.connect(database='postgres', host='localhost', port=5432, user='postgres', password='password') + query_result = PGDTransaction.delete( + connection_object=connection_object, + main_table='employees', + where=[ + {'column_name': 'id', 'value': '6'}, + ], + ) + + result = asyncio.run(query_result) + print(result) + + # None + # will delete the employee with id 6 + + # save changes to the database + connection_object.commit() + ``` + +
\ No newline at end of file diff --git a/src/postgres_dynamic/pgd_connection.py b/src/postgres_dynamic/pgd_connection.py index b10b2cf..fec3991 100644 --- a/src/postgres_dynamic/pgd_connection.py +++ b/src/postgres_dynamic/pgd_connection.py @@ -5,11 +5,11 @@ def __init__(self, connection_string: dict, query: str, values: tuple) -> None: self.query = query self.values = values self.connection = psycopg2.connect( - host = connection_string['pg_host'], - port = connection_string['pg_port'], - database = connection_string['pg_database'], - user = connection_string['pg_user'], - password = connection_string['pg_password'] + host = connection_string['PG_HOST'], + port = connection_string['PG_PORT'], + database = connection_string['PG_DATABASE'], + user = connection_string['PG_USER'], + password = connection_string['PG_PASSWORD'] ) self.cursor = self.connection.cursor() diff --git a/src/postgres_dynamic/pgd_get.py b/src/postgres_dynamic/pgd_get.py index 1571ee6..284bcee 100644 --- a/src/postgres_dynamic/pgd_get.py +++ b/src/postgres_dynamic/pgd_get.py @@ -1,6 +1,7 @@ from typing import Awaitable, Optional from datetime import datetime, timedelta from postgres_dynamic.pgd_connection import PGDConnection + class PGDGet: @classmethod async def get_one(cls, connection_string: dict, main_table: dict, where: list, join_table:list = [], column_name: list = None) -> Awaitable[dict]: @@ -10,14 +11,17 @@ async def get_one(cls, connection_string: dict, main_table: dict, where: list, j query = """ SELECT {column_name} FROM {main_table} {main_table_alias} {join_table} WHERE {where_query} """ + where_query='' for w in where: where_query += f' {w["column_name"]} {w["operator"]} %s' if w.get('operator') else f' {w["column_name"]} = %s' where_query += f' {w["conjunction"]}' if w.get('conjunction') else ' ' + join_query = '' for i in join_table: join_query += ' INNER JOIN' if i['join_method'] == 'INNER' else ' LEFT JOIN' if i['join_method'] == 'LEFT' else ' RIGHT JOIN' join_query += f' {i["table"]} {i["alias"]} ON {i["on"]}' + query = query.format(column_name=column_name, main_table=main_table['table'], main_table_alias=main_table['alias'] if main_table.get('alias') else '', join_table=join_query, where_query=where_query) where_values = tuple(wv['value'] for wv in where) values = where_values @@ -32,22 +36,30 @@ async def get_one(cls, connection_string: dict, main_table: dict, where: list, j raise e @classmethod - async def get_all(cls, connection_string: dict, main_table: dict, where: list, join_table:list = [], order: dict = {}, column_name: list = None, limit: Optional[int] = None, offset: Optional[int] = None) -> Awaitable[dict]: + async def get_all(cls, connection_string: dict, main_table: dict, where: list = [], join_table:list = [], order: dict = {}, column_name: list = None, limit: Optional[int] = None, offset: Optional[int] = None) -> Awaitable[dict]: try: result = [] column_name = ','.join(column_name) if column_name else '*' query = """ - SELECT {column_name} FROM {main_table} {main_table_alias} {join_table} WHERE {where_query} {order_query} LIMIT %s OFFSET %s + SELECT {column_name} FROM {main_table} {main_table_alias} {join_table} {where_query} {order_query} LIMIT %s OFFSET %s """ - where_query='' - for w in where: - where_query += f' {w["column_name"]} {w["operator"]} %s' if w.get('operator') else f' {w["column_name"]} = %s' - where_query += f' {w["conjunction"]}' if w.get('conjunction') else ' ' - order_query = 'ORDER BY ' + ''.join([key + f' {order[key]}' for key in order.keys()]) if len(order) == 1 else ' , '.join([key + f' {order[key]}' for key in order.keys()]) + where_query = '' + if where: + where_query += 'WHERE ' + for w in where: + where_query += f' {w["column_name"]} {w["operator"]} %s' if w.get('operator') else f' {w["column_name"]} = %s' + where_query += f' {w["conjunction"]}' if w.get('conjunction') else ' ' + + order_query = '' + if order: + order_query = 'ORDER BY ' + order_query += ''.join([f'{k} {v}' for k,v in order.items()]) if len(order) == 1 else ', '.join([f'{k} {v}' for k,v in order.items()]) + join_query = '' for i in join_table: join_query += ' INNER JOIN' if i['join_method'] == 'INNER' else ' LEFT JOIN' if i['join_method'] == 'LEFT' else ' RIGHT JOIN' join_query += f' {i["table"]} {i["alias"]} ON {i["on"]}' + query = query.format(column_name=column_name, main_table=main_table['table'], main_table_alias=main_table['alias'] if main_table.get('alias') else '', join_table=join_query, where_query=where_query, order_query=order_query) where_values = tuple(wv['value'] for wv in where) offset_value = (limit*(offset-1)) if offset else None @@ -64,20 +76,25 @@ async def get_all(cls, connection_string: dict, main_table: dict, where: list, j raise e @classmethod - async def get_count(cls, connection_string: dict, main_table: dict, where: list, join_table:list = []) -> Awaitable[dict]: + async def get_count(cls, connection_string: dict, main_table: dict, where: list = [], join_table:list = []) -> Awaitable[dict]: try: result = [] query = """ - SELECT COUNT(*) FROM {main_table} {main_table_alias} {join_table} WHERE {where_query} + SELECT COUNT(*) FROM {main_table} {main_table_alias} {join_table} {where_query} """ - where_query='' - for w in where: - where_query += f' {w["column_name"]} {w["operator"]} %s' if w.get('operator') else f' {w["column_name"]} = %s' - where_query += f' {w["conjunction"]}' if w.get('conjunction') else ' ' + + where_query = '' + if where: + where_query += 'WHERE ' + for w in where: + where_query += f' {w["column_name"]} {w["operator"]} %s' if w.get('operator') else f' {w["column_name"]} = %s' + where_query += f' {w["conjunction"]}' if w.get('conjunction') else ' ' + join_query = '' for i in join_table: join_query += ' INNER JOIN' if i['join_method'] == 'INNER' else ' LEFT JOIN' if i['join_method'] == 'LEFT' else ' RIGHT JOIN' join_query += f' {i["table"]} {i["alias"]} ON {i["on"]}' + query = query.format(main_table=main_table['table'], main_table_alias=main_table['alias'] if main_table.get('alias') else '', join_table=join_query, where_query=where_query) where_values = tuple(wv['value'] if isinstance(wv['value'], str) else tuple(wv['value']) for wv in where ) values = where_values