From edbede84910c9e6f3a462882df185d1d659a1f08 Mon Sep 17 00:00:00 2001 From: Luke Dziewanowski Date: Thu, 21 Oct 2021 09:03:56 +0200 Subject: [PATCH 01/15] Add alias with backward compability --- .gitignore | 8 +- run_tests.sh | 30 ++++++ src/DatabaseLibrary/assertion.py | 56 ++++++----- src/DatabaseLibrary/connection_manager.py | 115 ++++++++++++++-------- src/DatabaseLibrary/query.py | 89 ++++++++++------- test/Aliases_SQLite3_DB_Tests.robot | 55 +++++++++++ test/tests/_old/DB2SQL_DB_Tests.robot | 29 ++++-- test/tests/_old/MySQL_DB_Tests.robot | 43 ++------ test/tests/_old/PostgreSQL_DB_Tests.robot | 8 +- test/tests/_old/PyODBC_DB_Tests.robot | 5 + 10 files changed, 283 insertions(+), 155 deletions(-) create mode 100755 run_tests.sh create mode 100644 test/Aliases_SQLite3_DB_Tests.robot diff --git a/.gitignore b/.gitignore index a9f49bcb..0c61da21 100644 --- a/.gitignore +++ b/.gitignore @@ -6,9 +6,9 @@ build/ .py*/ **/my_db_test.db logs -**/output.xml -**/interactive_console_output.xml -**/log.html -**/report.html +interactive_console_output.xml +log.html +output.xml +report.html venv .runNumber diff --git a/run_tests.sh b/run_tests.sh new file mode 100755 index 00000000..8d7caf40 --- /dev/null +++ b/run_tests.sh @@ -0,0 +1,30 @@ +#!/bin/bash -xe + +function startup { + docker-compose up -d + sleep 10 +} + +function cleanup { + docker-compose down +} + +if [[ $1 == "clean" ]] +then + trap cleanup EXIT + startup + sleep 10 +fi + +export MYSQL_PORT=`docker-compose port mysqldb 3306 | cut -d ":" -f 2` +export POSTGRESQL_PORT=`docker-compose port postgresqldb 5432 | cut -d ":" -f 2` +export DB2_PORT=`docker-compose port db2db 50000 | cut -d ":" -f 2` + +yq e -i ' + .MYSQL_DBPort = env(MYSQL_PORT) | + .POSTGRESQL_DBPort = env(POSTGRESQL_PORT) | + .DB2_DBPort = env(DB2_PORT) +' test/DB_Variables.yaml + + +robot --randomize none -V test/DB_Variables.yaml -i main test diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 185c2ba2..ea586943 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -21,7 +21,7 @@ class Assertion: Assertion handles all the assertions of Database Library. """ - def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None): + def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): """ Check if any row would be returned by given the input `selectStatement`. If there are no results, then this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction @@ -34,6 +34,7 @@ def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = Fal When you have the following assertions in your robot | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | Then you will get the following: | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # PASS | @@ -46,12 +47,12 @@ def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = Fal | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | msg=my error message | """ logger.info(f"Executing : Check If Exists In Database | {selectStatement}") - if not self.query(selectStatement, sansTran): + if not self.query(selectStatement, sansTran, alias=alias): raise AssertionError( - msg or f"Expected to have have at least one row, " f"but got 0 rows from: '{selectStatement}'" + msg or f"Expected to have have at least one row, but got 0 rows from: '{selectStatement}'" ) - def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None): + def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): """ This is the negation of `check_if_exists_in_database`. @@ -66,6 +67,7 @@ def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = When you have the following assertions in your robot | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | Then you will get the following: | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # PASS | @@ -78,13 +80,13 @@ def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | """ logger.info(f"Executing : Check If Not Exists In Database | {selectStatement}") - query_results = self.query(selectStatement, sansTran) + query_results = self.query(selectStatement, sansTran, alias=alias) if query_results: raise AssertionError( msg or f"Expected to have have no rows from '{selectStatement}', but got some rows: {query_results}" ) - def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None): + def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): """ Check if any rows are returned from the submitted `selectStatement`. If there are, then this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction commit or @@ -97,6 +99,7 @@ def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Opti When you have the following assertions in your robot | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | alias=my_alias | Then you will get the following: | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | @@ -109,12 +112,12 @@ def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Opti | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | """ logger.info(f"Executing : Row Count Is 0 | {selectStatement}") - num_rows = self.row_count(selectStatement, sansTran) + num_rows = self.row_count(selectStatement, sansTran, alias=alias) if num_rows > 0: raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'") def row_count_is_equal_to_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None + self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ Check if the number of rows returned from `selectStatement` is equal to the value submitted. If not, then this @@ -129,6 +132,7 @@ def row_count_is_equal_to_x( When you have the following assertions in your robot | Row Count Is Equal To X | SELECT id FROM person | 1 | | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | Then you will get the following: | Row Count Is Equal To X | SELECT id FROM person | 1 | # FAIL | @@ -141,14 +145,14 @@ def row_count_is_equal_to_x( | Row Count Is Equal To X | SELECT id FROM person | 1 | msg=my error message | """ logger.info(f"Executing : Row Count Is Equal To X | {selectStatement} | {numRows}") - num_rows = self.row_count(selectStatement, sansTran) + num_rows = self.row_count(selectStatement, sansTran, alias=alias) if num_rows != int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected {numRows} rows, but {num_rows} were returned from: '{selectStatement}'" ) def row_count_is_greater_than_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None + self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ Check if the number of rows returned from `selectStatement` is greater than the value submitted. If not, then @@ -163,6 +167,7 @@ def row_count_is_greater_than_x( When you have the following assertions in your robot | Row Count Is Greater Than X | SELECT id FROM person | 1 | | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | Then you will get the following: | Row Count Is Greater Than X | SELECT id FROM person | 1 | # PASS | @@ -175,14 +180,14 @@ def row_count_is_greater_than_x( | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | msg=my error message | """ logger.info(f"Executing : Row Count Is Greater Than X | {selectStatement} | {numRows}") - num_rows = self.row_count(selectStatement, sansTran) + num_rows = self.row_count(selectStatement, sansTran, alias=alias) if num_rows <= int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected more than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" ) def row_count_is_less_than_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None + self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ Check if the number of rows returned from `selectStatement` is less than the value submitted. If not, then this @@ -197,6 +202,7 @@ def row_count_is_less_than_x( When you have the following assertions in your robot | Row Count Is Less Than X | SELECT id FROM person | 3 | | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | alias=my_alias | Then you will get the following: | Row Count Is Less Than X | SELECT id FROM person | 3 | # PASS | @@ -209,13 +215,13 @@ def row_count_is_less_than_x( | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | msg=my error message | """ logger.info(f"Executing : Row Count Is Less Than X | {selectStatement} | {numRows}") - num_rows = self.row_count(selectStatement, sansTran) + num_rows = self.row_count(selectStatement, sansTran, alias=alias) if num_rows >= int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" ) - def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional[str] = None): + def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): """ Check if the table given exists in the database. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. The default error message can be overridden with the `msg` argument. @@ -228,6 +234,7 @@ def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional Then you will get the following: | Table Must Exist | person | # PASS | | Table Must Exist | first_name | # FAIL | + | Table Must Exist | first_name | alias=my_alias | Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Table Must Exist | person | True | @@ -236,30 +243,31 @@ def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional | Table Must Exist | first_name | msg=my error message | """ logger.info(f"Executing : Table Must Exist | {tableName}") - if self.db_api_module_name in ["cx_Oracle", "oracledb"]: + _, db_api_module_name = self._cache.switch(alias) + if db_api_module_name in ["cx_Oracle", "oracledb"]: query = ( "SELECT * FROM all_objects WHERE object_type IN ('TABLE','VIEW') AND " f"owner = SYS_CONTEXT('USERENV', 'SESSION_USER') AND object_name = UPPER('{tableName}')" ) - table_exists = self.row_count(query, sansTran) > 0 - elif self.db_api_module_name in ["sqlite3"]: + table_exists = self.row_count(query, sansTran, alias=alias) > 0 + elif db_api_module_name in ["sqlite3"]: query = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}' COLLATE NOCASE" - table_exists = self.row_count(query, sansTran) > 0 - elif self.db_api_module_name in ["ibm_db", "ibm_db_dbi"]: + table_exists = self.row_count(query, sansTran, alias=alias) > 0 + elif db_api_module_name in ["ibm_db", "ibm_db_dbi"]: query = f"SELECT name FROM SYSIBM.SYSTABLES WHERE type='T' AND name=UPPER('{tableName}')" - table_exists = self.row_count(query, sansTran) > 0 - elif self.db_api_module_name in ["teradata"]: + table_exists = self.row_count(query, sansTran, alias=alias) > 0 + elif db_api_module_name in ["teradata"]: query = f"SELECT TableName FROM DBC.TablesV WHERE TableKind='T' AND TableName='{tableName}'" - table_exists = self.row_count(query, sansTran) > 0 + table_exists = self.row_count(query, sansTran, alias=alias) > 0 else: try: query = f"SELECT * FROM information_schema.tables WHERE table_name='{tableName}'" - table_exists = self.row_count(query, sansTran) > 0 + table_exists = self.row_count(query, sansTran, alias=alias) > 0 except: logger.info("Database doesn't support information schema, try using a simple SQL request") try: query = f"SELECT 1 from {tableName} where 1=0" - self.row_count(query, sansTran) + self.row_count(query, sansTran, alias=alias) table_exists = True except: table_exists = False diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index f4d05e38..13259b05 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -21,6 +21,7 @@ import configparser as ConfigParser from robot.api import logger +from robot.utils import ConnectionCache class ConnectionManager: @@ -29,12 +30,8 @@ class ConnectionManager: """ def __init__(self): - """ - Initializes _dbconnection to None. - """ - self._dbconnection = None - self.db_api_module_name = None self.omit_trailing_semicolon = False + self._cache = ConnectionCache("No sessions created") def connect_to_database( self, @@ -48,6 +45,7 @@ def connect_to_database( dbDriver: Optional[str] = None, dbConfigFile: Optional[str] = None, driverMode: Optional[str] = None, + alias: Optional[str] = "default", ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to @@ -60,7 +58,7 @@ def connect_to_database( - _thick,lib_dir=_ Optionally, you can specify a `dbConfigFile` wherein it will load the - default property values for `dbapiModuleName`, `dbName` `dbUsername` + alias (or alias will be "default") property values for `dbapiModuleName`, `dbName` `dbUsername` and `dbPassword` (note: specifying `dbapiModuleName`, `dbName` `dbUsername` or `dbPassword` directly will override the properties of the same key in `dbConfigFile`). If no `dbConfigFile` is specified, it @@ -70,7 +68,7 @@ def connect_to_database( your database credentials. Example db.cfg file - | [default] + | [alias] | dbapiModuleName=pymysqlforexample | dbName=yourdbname | dbUsername=yourusername @@ -81,6 +79,7 @@ def connect_to_database( Example usage: | # explicitly specifies all db property values | | Connect To Database | psycopg2 | my_db | postgres | s3cr3t | tiger.foobar.com | 5432 | + | Connect To Database | psycopg2 | my_db | postgres | s3cr3t | tiger.foobar.com | 5432 | alias=my_alias | | # loads all property values from default.cfg | | Connect To Database | dbConfigFile=default.cfg | @@ -100,18 +99,18 @@ def connect_to_database( config = ConfigParser.ConfigParser() config.read([dbConfigFile]) - dbapiModuleName = dbapiModuleName or config.get("default", "dbapiModuleName") - dbName = dbName or config.get("default", "dbName") - dbUsername = dbUsername or config.get("default", "dbUsername") - dbPassword = dbPassword if dbPassword is not None else config.get("default", "dbPassword") - dbHost = dbHost or config.get("default", "dbHost") or "localhost" - dbPort = int(dbPort or config.get("default", "dbPort")) + dbapiModuleName = dbapiModuleName or config.get(alias, "dbapiModuleName") + dbName = dbName or config.get(alias, "dbName") + dbUsername = dbUsername or config.get(alias, "dbUsername") + dbPassword = dbPassword if dbPassword is not None else config.get(alias, "dbPassword") + dbHost = dbHost or config.get(alias, "dbHost") or "localhost" + dbPort = int(dbPort or config.get(alias, "dbPort")) if dbapiModuleName == "excel" or dbapiModuleName == "excelrw": - self.db_api_module_name = "pyodbc" + db_api_module_name = "pyodbc" db_api_2 = importlib.import_module("pyodbc") else: - self.db_api_module_name = dbapiModuleName + db_api_module_name = dbapiModuleName db_api_2 = importlib.import_module(dbapiModuleName) if dbapiModuleName in ["MySQLdb", "pymysql"]: @@ -120,7 +119,7 @@ def connect_to_database( f"Connecting using : {dbapiModuleName}.connect(" f"db={dbName}, user={dbUsername}, passwd=***, host={dbHost}, port={dbPort}, charset={dbCharset})" ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( db=dbName, user=dbUsername, passwd=dbPassword, @@ -134,7 +133,7 @@ def connect_to_database( f"Connecting using : {dbapiModuleName}.connect(" f"database={dbName}, user={dbUsername}, password=***, host={dbHost}, port={dbPort})" ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( database=dbName, user=dbUsername, password=dbPassword, @@ -151,14 +150,14 @@ def connect_to_database( else: con_str += f"SERVER={dbHost},{dbPort}" logger.info(f'Connecting using : {dbapiModuleName}.connect({con_str.replace(dbPassword, "***")})') - self._dbconnection = db_api_2.connect(con_str) + db_connection = db_api_2.connect(con_str) elif dbapiModuleName in ["excel"]: logger.info( f"Connecting using : {dbapiModuleName}.connect(" f"DRIVER={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DBQ={dbName};" f'ReadOnly=1;Extended Properties="Excel 8.0;HDR=YES";)' ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( f"DRIVER={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DBQ={dbName};" f'ReadOnly=1;Extended Properties="Excel 8.0;HDR=YES";)', autocommit=True, @@ -169,7 +168,7 @@ def connect_to_database( f"DRIVER={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DBQ={dbName};" f'ReadOnly=0;Extended Properties="Excel 8.0;HDR=YES";)', ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( f"DRIVER={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DBQ={dbName};" f'ReadOnly=0;Extended Properties="Excel 8.0;HDR=YES";)', autocommit=True, @@ -178,7 +177,7 @@ def connect_to_database( dbPort = dbPort or 50000 conn_str = f"DATABASE={dbName};HOSTNAME={dbHost};PORT={dbPort};PROTOCOL=TCPIP;UID={dbUsername};" logger.info(f"Connecting using : {dbapiModuleName}.connect(" f"{conn_str};PWD=***;)") - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( f"{conn_str};PWD={dbPassword};", "", "", @@ -189,7 +188,7 @@ def connect_to_database( logger.info( f"Connecting using: {dbapiModuleName}.connect(user={dbUsername}, password=***, dsn={oracle_dsn})" ) - self._dbconnection = db_api_2.connect(user=dbUsername, password=dbPassword, dsn=oracle_dsn) + db_connection = db_api_2.connect(user=dbUsername, password=dbPassword, dsn=oracle_dsn) self.omit_trailing_semicolon = True elif dbapiModuleName in ["oracledb"]: dbPort = dbPort or 1521 @@ -215,10 +214,10 @@ def connect_to_database( f"Connecting using: {dbapiModuleName}.connect(" f"user={dbUsername}, password=***, params={oracle_connection_params})" ) - self._dbconnection = db_api_2.connect(user=dbUsername, password=dbPassword, params=oracle_connection_params) - assert self._dbconnection.thin == oracle_thin_mode, ( + db_connection = db_api_2.connect(user=dbUsername, password=dbPassword, params=oracle_connection_params) + assert db_connection.thin == oracle_thin_mode, ( "Expected oracledb to run in thin mode: {oracle_thin_mode}, " - f"but the connection has thin mode: {self._dbconnection.thin}" + f"but the connection has thin mode: {db_connection.thin}" ) self.omit_trailing_semicolon = True elif dbapiModuleName in ["teradata"]: @@ -228,7 +227,7 @@ def connect_to_database( f"Connecting using : {dbapiModuleName}.connect(" f"database={dbName}, user={dbUsername}, password=***, host={dbHost}, port={dbPort})" ) - self._dbconnection = teradata_udaExec.connect( + db_connection = teradata_udaExec.connect( method="odbc", system=dbHost, database=dbName, @@ -243,7 +242,7 @@ def connect_to_database( f"Connecting using : {dbapiModuleName}.connect(" f"database={dbName}, user={dbUsername}, password=***, host={dbHost}, port={dbPort})" ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( database=dbName, user=dbUsername, password=dbPassword, @@ -255,16 +254,17 @@ def connect_to_database( f"Connecting using : {dbapiModuleName}.connect(" f"database={dbName}, user={dbUsername}, password=***, host={dbHost}, port={dbPort}) " ) - self._dbconnection = db_api_2.connect( + db_connection = db_api_2.connect( database=dbName, user=dbUsername, password=dbPassword, host=dbHost, port=dbPort, ) + self._cache.register((db_connection, db_api_module_name), alias=alias) def connect_to_database_using_custom_params( - self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "" + self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: Optional[str] = "default" ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to @@ -281,7 +281,7 @@ def connect_to_database_using_custom_params( | Connect To Database Using Custom Params | sqlite3 | database="./my_database.db", isolation_level=None | """ db_api_2 = importlib.import_module(dbapiModuleName) - self.db_api_module_name = dbapiModuleName + db_api_module_name = dbapiModuleName db_connect_string = f"db_api_2.connect({db_connect_string})" @@ -298,10 +298,11 @@ def connect_to_database_using_custom_params( f"{connection_string_with_hidden_pass})" ) - self._dbconnection = eval(db_connect_string) + db_connection = eval(db_connect_string) + self._cache.register((db_connection, db_api_module_name), alias=alias) def connect_to_database_using_custom_connection_string( - self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "" + self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: Optional[str] = "default" ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to @@ -316,14 +317,15 @@ def connect_to_database_using_custom_connection_string( | Connect To Database Using Custom Connection String | oracledb | username/pass@localhost:1521/orclpdb | """ db_api_2 = importlib.import_module(dbapiModuleName) - self.db_api_module_name = dbapiModuleName + db_api_module_name = dbapiModuleName logger.info( f"Executing : Connect To Database Using Custom Connection String : {dbapiModuleName}.connect(" f"'{db_connect_string}')" ) - self._dbconnection = db_api_2.connect(db_connect_string) + db_connection = db_api_2.connect(db_connect_string) + self._cache.register((db_connection, db_api_module_name), alias=alias) - def disconnect_from_database(self, error_if_no_connection: bool = False): + def disconnect_from_database(self, error_if_no_connection: bool = False, alias: Optional[str] = "default"): """ Disconnects from the database. @@ -333,18 +335,35 @@ def disconnect_from_database(self, error_if_no_connection: bool = False): Example usage: | Disconnect From Database | # disconnects from current connection to the database | + | Disconnect From Database | alias=my_alias | # disconnects from current connection to the database | """ logger.info("Executing : Disconnect From Database") - if self._dbconnection is None: + try: + db_connection, _ = self._cache.switch(alias) + except RuntimeError: # Non-existing index or alias + db_connection = None + if db_connection: + db_connection.close() + else: log_msg = "No open database connection to close" if error_if_no_connection: - raise ConnectionError(log_msg) + raise ConnectionError(log_msg) from None logger.info(log_msg) - else: - self._dbconnection.close() - self._dbconnection = None - def set_auto_commit(self, autoCommit: bool = True): + def disconnect_from_all_databases(self): + """ + Disconnects from all the databases. + + For example: + | Disconnect From All Databases | # disconnects from all connections to the database | + """ + logger.info("Executing : Disconnect From All Databases") + for db_connection, _ in self._cache: + if db_connection: + db_connection.close() + self._cache.empty_cache() + + def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = "default"): """ Turn the autocommit on the database connection ON or OFF. @@ -357,8 +376,20 @@ def set_auto_commit(self, autoCommit: bool = True): Example usage: | # Default behaviour, sets auto commit to true | Set Auto Commit + | Set Auto Commit | alias=my_alias | | # Explicitly set the desired state | Set Auto Commit | False """ logger.info("Executing : Set Auto Commit") - self._dbconnection.autocommit = autoCommit + db_connection, _ = self._cache.switch(alias) + db_connection.autocommit = autoCommit + + def switch_database(self, alias): + """ + Switch default database. + + Example: + | Switch Database | my_alias | + | Switch Database | alias=my_alias | + """ + self._cache.switch(alias) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index f4c47867..a59ba536 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -24,7 +24,7 @@ class Query: Query handles all the querying done by the Database Library. """ - def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False): + def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None): """ Uses the input `selectStatement` to query for the values that will be returned as a list of tuples. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -43,6 +43,7 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool When you do the following: | @{queryResults} | Query | SELECT * FROM person | + | @{queryResults} | Query | SELECT * FROM person | alias=my_alias | | Log Many | @{queryResults} | You will get the following: @@ -58,10 +59,11 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Query | SELECT * FROM person | True | """ + db_connection, _ = self._cache.switch(alias) cur = None try: - cur = self._dbconnection.cursor() - logger.info(f"Executing : Query | {selectStatement}") + cur = db_connection.cursor() + logger.info(f"Executing : Query | {selectStatement} ") self.__execute_sql(cur, selectStatement) all_rows = cur.fetchall() if returnAsDict: @@ -70,9 +72,9 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool return all_rows finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def row_count(self, selectStatement: str, sansTran: bool = False): + def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ Uses the input `selectStatement` to query the database and returns the number of rows from the query. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -84,6 +86,7 @@ def row_count(self, selectStatement: str, sansTran: bool = False): When you do the following: | ${rowCount} | Row Count | SELECT * FROM person | + | ${rowCount} | Row Count | SELECT * FROM person | alias=my_alias | | Log | ${rowCount} | You will get the following: @@ -99,20 +102,21 @@ def row_count(self, selectStatement: str, sansTran: bool = False): Using optional `sansTran` to run command without an explicit transaction commit or rollback: | ${rowCount} | Row Count | SELECT * FROM person | True | """ + db_connection, db_api_module_name = self._cache.switch(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.cursor() logger.info(f"Executing : Row Count | {selectStatement}") self.__execute_sql(cur, selectStatement) data = cur.fetchall() - if self.db_api_module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: + if db_api_module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: return len(data) return cur.rowcount finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def description(self, selectStatement: str, sansTran: bool = False): + def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ Uses the input `selectStatement` to query a table in the db which will be used to determine the description. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -123,6 +127,7 @@ def description(self, selectStatement: str, sansTran: bool = False): When you do the following: | @{queryResults} | Description | SELECT * FROM person | + | @{queryResults} | Description | SELECT * FROM person | alias=my_alias | | Log Many | @{queryResults} | You will get the following: @@ -133,9 +138,10 @@ def description(self, selectStatement: str, sansTran: bool = False): Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Description | SELECT * FROM person | True | """ + db_connection, _ = self._cache.switch(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.cursor() logger.info("Executing : Description | {selectStatement}") self.__execute_sql(cur, selectStatement) description = list(cur.description) @@ -145,9 +151,9 @@ def description(self, selectStatement: str, sansTran: bool = False): return description finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False): + def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, alias: Optional[str] = None): """ Delete all the rows within a given table. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -156,6 +162,7 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False): When you do the following: | Delete All Rows From Table | person | + | Delete All Rows From Table | person | alias=my_alias | If all the rows can be successfully deleted, then you will get: | Delete All Rows From Table | person | # PASS | @@ -166,23 +173,24 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False): Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Delete All Rows From Table | person | True | """ + db_connection, _ = self._cache.switch(alias) cur = None query = f"DELETE FROM {tableName}" try: - cur = self._dbconnection.cursor() + cur = db_connection.cursor() logger.info(f"Executing : Delete All Rows From Table | {query}") result = self.__execute_sql(cur, query) if result is not None: if not sansTran: - self._dbconnection.commit() + db_connection.commit() return result if not sansTran: - self._dbconnection.commit() + db_connection.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False): + def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, alias: Optional[str] = None): """ Executes the content of the `sqlScriptFileName` as SQL commands. Useful for setting the database to a known state before running your tests, or clearing out your test data after running each a test. Set optional input @@ -192,6 +200,7 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False): Sample usage : | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-setup.sql | + | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | alias=my_alias | | #interesting stuff here | | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-teardown.sql | | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-teardown.sql | @@ -240,11 +249,12 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False): Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | True | """ + db_connection, _ = self._cache.switch(alias) with open(sqlScriptFileName, encoding="UTF-8") as sql_file: cur = None try: statements_to_execute = [] - cur = self._dbconnection.cursor() + cur = db_connection.cursor() logger.info(f"Executing : Execute SQL Script | {sqlScriptFileName}") current_statement = "" inside_statements_group = False @@ -300,12 +310,12 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False): omit_semicolon = not statement.lower().endswith("end;") self.__execute_sql(cur, statement, omit_semicolon) if not sansTran: - self._dbconnection.commit() + db_connection.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def execute_sql_string(self, sqlString: str, sansTran: bool = False): + def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None): """ Executes the sqlString as SQL commands. Useful to pass arguments to your sql. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -314,6 +324,7 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False): For example: | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | + | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | alias=my_alias | For example with an argument: | Execute Sql String | SELECT * FROM person WHERE first_name = ${FIRSTNAME} | @@ -321,18 +332,19 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False): Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | True | """ + db_connection, _ = self._cache.switch(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.cursor() logger.info(f"Executing : Execute SQL String | {sqlString}") self.__execute_sql(cur, sqlString) if not sansTran: - self._dbconnection.commit() + db_connection.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() - def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = None, sansTran: bool = False): + def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = None, sansTran: bool = False, alias: Optional[str] = None): """ Calls a stored procedure `spName` with the `spParams` - a *list* of parameters the procedure requires. Use the special *CURSOR* value for OUT params, which should receive result sets - @@ -371,20 +383,21 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{Param values} @{Result sets} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | ${Params} | True | """ + db_connection, db_api_module_name = self._cache.switch(alias) if spParams is None: spParams = [] cur = None try: logger.info(f"Executing : Call Stored Procedure | {spName} | {spParams}") - if self.db_api_module_name == "pymssql": - cur = self._dbconnection.cursor(as_dict=False) + if db_api_module_name == "pymssql": + cur = db_connection.cursor(as_dict=False) else: - cur = self._dbconnection.cursor() + cur = db_connection.cursor() param_values = [] result_sets = [] - if self.db_api_module_name == "pymysql": + if db_api_module_name == "pymysql": cur.callproc(spName, spParams) # first proceed the result sets if available @@ -401,22 +414,22 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non cur.execute(f"select @_{spName}_{i}") param_values.append(cur.fetchall()[0][0]) - elif self.db_api_module_name in ["oracledb", "cx_Oracle"]: + elif db_api_module_name in ["oracledb", "cx_Oracle"]: # check if "CURSOR" params were passed - they will be replaced # with cursor variables for storing the result sets params_substituted = spParams.copy() cursor_params = [] for i in range(0, len(spParams)): if spParams[i] == "CURSOR": - cursor_param = self._dbconnection.cursor() + cursor_param = db_connection.cursor() params_substituted[i] = cursor_param cursor_params.append(cursor_param) param_values = cur.callproc(spName, params_substituted) for result_set in cursor_params: result_sets.append(list(result_set)) - elif self.db_api_module_name in ["psycopg2", "psycopg3"]: - cur = self._dbconnection.cursor() + elif db_api_module_name in ["psycopg2", "psycopg3"]: + cur = db_connection.cursor() # check if "CURSOR" params were passed - they will be replaced # with cursor variables for storing the result sets params_substituted = spParams.copy() @@ -433,7 +446,7 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non result_set = cur.fetchall() result_sets.append(list(result_set)) else: - if self.db_api_module_name in ["psycopg3"]: + if db_api_module_name in ["psycopg3"]: result_sets_available = True while result_sets_available: result_sets.append(list(cur.fetchall())) @@ -444,10 +457,10 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non else: logger.info( - f"CAUTION! Calling a stored procedure for '{self.db_api_module_name}' is not tested, " + f"CAUTION! Calling a stored procedure for '{db_api_module_name}' is not tested, " "results might be invalid!" ) - cur = self._dbconnection.cursor() + cur = db_connection.cursor() param_values = cur.callproc(spName, spParams) logger.info("Reading the procedure results..") result_sets_available = True @@ -463,12 +476,12 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non result_sets_available = False if not sansTran: - self._dbconnection.commit() + db_connection.commit() return param_values, result_sets finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.rollback() def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Optional[bool] = None): """ diff --git a/test/Aliases_SQLite3_DB_Tests.robot b/test/Aliases_SQLite3_DB_Tests.robot new file mode 100644 index 00000000..40d7d725 --- /dev/null +++ b/test/Aliases_SQLite3_DB_Tests.robot @@ -0,0 +1,55 @@ +*** Settings *** +Library DatabaseLibrary +Library OperatingSystem +Force Tags main db smoke + +*** Variables *** +${DBName1} my_db_test1 +${DBName2} my_db_test2 + +*** Keywords *** +Remove DB file if exists + [Arguments] ${DB_FILE} + Run Keyword And Ignore Error Remove File ${DB_FILE} + File Should Not Exist ${DB_FILE} + Comment Sleep 1s + +*** Test Cases *** +Remove old DB if exists + Remove DB file if exists ${CURDIR}/${DBName1}.db + Remove DB file if exists ${CURDIR}/${DBName2}.db + +Connect to SQLiteDB + Comment Connect To Database Using Custom Params sqlite3 database='path_to_dbfile\dbname.db' + Connect To Database Using Custom Params sqlite3 database="${CURDIR}/${DBName1}.db", isolation_level=None alias=db1 + Connect To Database Using Custom Params sqlite3 database="${CURDIR}/${DBName2}.db", isolation_level=None alias=db2 + +Create person table + ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar,last_name varchar); alias=db2 + Log ${output} + Should Be Equal As Strings ${output} None + +Create foobar table + ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar unique) alias=db1 + Log ${output} + Should Be Equal As Strings ${output} None + +Table Must Exist - person + Table Must Exist person alias=db2 + +Table Shouldn't Exist - person + Run Keyword And Expect Error Table 'person' does not exist in the db Table Must Exist person alias=db1 + +Table Shouldn't Exist - foobar + Run Keyword And Expect Error Table 'foobar' does not exist in the db Table Must Exist foobar alias=db2 + +Switch database without alias + Switch Database db2 + Table Must Exist person + Run Keyword And Expect Error Table 'foobar' does not exist in the db Table Must Exist foobar + +Disconnect from database db1 + Disconnect From Database alias=db1 + +Disconnect from all databases + Disconnect From All Databases diff --git a/test/tests/_old/DB2SQL_DB_Tests.robot b/test/tests/_old/DB2SQL_DB_Tests.robot index 46e50f5e..e3e4d487 100644 --- a/test/tests/_old/DB2SQL_DB_Tests.robot +++ b/test/tests/_old/DB2SQL_DB_Tests.robot @@ -1,8 +1,9 @@ *** Settings *** Suite Setup Connect To Database ibm_db_dbi ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} Suite Teardown Disconnect From Database -Resource DB2SQL_DB_Conf.txt Library DatabaseLibrary +Library Collections +Force Tags optional *** Test Cases *** Create person table @@ -11,8 +12,8 @@ Create person table Should Be Equal As Strings ${output} None Execute SQL Script - Insert Data person table - Comment ${output} = Execute SQL Script ./my_db_test_insertData.sql - ${output} = Execute SQL Script ../test/my_db_test_insertData.sql + Comment ${output} = Execute SQL Script ${CURDIR}/my_db_test_insertData.sql + ${output} = Execute SQL Script ${CURDIR}/my_db_test_insertData.sql Log ${output} Should Be Equal As Strings ${output} None @@ -53,11 +54,20 @@ Verify person Description @{queryResults} = Description SELECT * FROM person fetch first 1 rows only; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output} ['ID', DBAPITypeObject(['NUM', 'DECIMAL', 'DEC', 'NUMERIC']), 12, 12, 10, 0, True] + Should Be Equal As Strings ${output[0]} ID + ${expected}= Evaluate ['DEC', 'NUMERIC', 'DECIMAL', 'NUM'] + Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True + Should Be Equal As Strings ${output[2:]} [12, 12, 10, 0, True] ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output} ['FIRST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] + Should Be Equal As Strings ${output[0]} FIRST_NAME + ${expected}= Evaluate ['VARCHAR', 'CHARACTER VARYING', 'STRING', 'CHARACTER', 'CHAR', 'CHAR VARYING'] + Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True + Should Be Equal As Strings ${output[2:]} [30, 30, 30, 0, True] ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output} ['LAST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] + Should Be Equal As Strings ${output[0]} LAST_NAME + ${expected}= Evaluate ['CHAR', 'CHAR VARYING', 'VARCHAR', 'CHARACTER VARYING', 'CHARACTER', 'STRING'] + Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True + Should Be Equal As Strings ${output[2:]} [30, 30, 30, 0, True] ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 3 @@ -75,8 +85,8 @@ Verify Query - Get results as a list of dictionaries [Tags] db smoke ${output} = Query SELECT * FROM person; \ True Log ${output} - Should Be Equal As Strings &{output[0]}[first_name] Franz Allan - Should Be Equal As Strings &{output[1]}[first_name] Jerry + Should Be Equal As Strings ${output[0]['FIRST_NAME']} Franz Allan + Should Be Equal As Strings ${output[1]['FIRST_NAME']} Jerry Insert Data Into Table foobar ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); @@ -97,3 +107,6 @@ Verify Query - Row Count foobar table 0 row Drop person and foobar table Execute SQL String DROP TABLE person; Execute SQL String DROP TABLE foobar; + +Disconnect from all databases + Disconnect From All Databases diff --git a/test/tests/_old/MySQL_DB_Tests.robot b/test/tests/_old/MySQL_DB_Tests.robot index c7f16d07..ac0114c4 100644 --- a/test/tests/_old/MySQL_DB_Tests.robot +++ b/test/tests/_old/MySQL_DB_Tests.robot @@ -3,6 +3,8 @@ Suite Setup Connect To Database ${DBModule} ${DBName} ${DBUser} Suite Teardown Disconnect From Database Library DatabaseLibrary Library OperatingSystem +Test Tags main db smoke + *** Variables *** ${DBHost} 127.0.0.1 @@ -13,66 +15,53 @@ ${DBUser} root *** Test Cases *** Create person table - [Tags] db smoke ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar(20),last_name varchar(20)); Log ${output} Should Be Equal As Strings ${output} None Execute SQL Script - Insert Data person table - [Tags] db smoke - Comment ${output} = Execute SQL Script ./${DBName}_insertData.sql - ${output} = Execute SQL Script ./my_db_test_insertData.sql + Comment ${output} = Execute SQL Script ./my_db_test_insertData.sql + ${output} = Execute SQL Script ${CURDIR}/my_db_test_insertData.sql Log ${output} Should Be Equal As Strings ${output} None Execute SQL String - Create Table - [Tags] db smoke ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar(20) unique) Log ${output} Should Be Equal As Strings ${output} None Check If Exists In DB - Franz Allan - [Tags] db smoke Check If Exists In Database SELECT id FROM person WHERE first_name = 'Franz Allan'; Check If Not Exists In DB - Joe - [Tags] db smoke Check If Not Exists In Database SELECT id FROM person WHERE first_name = 'Joe'; Table Must Exist - person - [Tags] db smoke Table Must Exist person Verify Row Count is 0 - [Tags] db smoke Row Count is 0 SELECT * FROM person WHERE first_name = 'NotHere'; Verify Row Count is Equal to X - [Tags] db smoke Row Count is Equal to X SELECT id FROM person; 2 Verify Row Count is Less Than X - [Tags] db smoke Row Count is Less Than X SELECT id FROM person; 3 Verify Row Count is Greater Than X - [Tags] db smoke Row Count is Greater Than X SELECT * FROM person; 1 Retrieve Row Count - [Tags] db smoke ${output} = Row Count SELECT id FROM person; Log ${output} Should Be Equal As Strings ${output} 2 Retrieve records from person table - [Tags] db smoke ${output} = Execute SQL String SELECT * FROM person; Log ${output} Should Be Equal As Strings ${output} None Verify person Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT * FROM person LIMIT 1; Log Many @{queryResults} @@ -86,7 +75,6 @@ Verify person Description Should Be Equal As Integers ${NumColumns} 3 Verify foobar Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT * FROM foobar LIMIT 1; Log Many @{queryResults} @@ -98,114 +86,97 @@ Verify foobar Description Should Be Equal As Integers ${NumColumns} 2 Verify Query - Row Count person table - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM person; Log ${output} Should Be Equal As Strings ${output} ((2,),) Verify Query - Row Count foobar table - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM foobar; Log ${output} Should Be Equal As Strings ${output} ((0,),) Verify Query - Get results as a list of dictionaries - [Tags] db smoke ${output} = Query SELECT * FROM person; \ True Log ${output} Should Be Equal As Strings ${output[0]}[first_name] Franz Allan Should Be Equal As Strings ${output[1]}[first_name] Jerry Verify Execute SQL String - Row Count person table - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM person; Log ${output} Should Be Equal As Strings ${output} None Verify Execute SQL String - Row Count foobar table - [Tags] db smoke ${output} = Execute SQL String SELECT COUNT(*) FROM foobar; Log ${output} Should Be Equal As Strings ${output} None Insert Data Into Table foobar - [Tags] db smoke ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); Log ${output} Should Be Equal As Strings ${output} None Verify Query - Row Count foobar table 1 row - [Tags] db smoke ${output} = Query SELECT COUNT(*) FROM foobar; Log ${output} Should Be Equal As Strings ${output} ((1,),) Verify Delete All Rows From Table - foobar - [Tags] db smoke Delete All Rows From Table foobar Comment Sleep 2s Verify Query - Row Count foobar table 0 row - [Tags] db smoke Row Count Is 0 SELECT * FROM foobar; Comment ${output} = Query SELECT COUNT(*) FROM foobar; Comment Log ${output} Comment Should Be Equal As Strings ${output} [(0,)] Begin first transaction - [Tags] db smoke ${output} = Execute SQL String SAVEPOINT first True Log ${output} Should Be Equal As Strings ${output} None Add person in first transaction - [Tags] db smoke ${output} = Execute SQL String INSERT INTO person VALUES(101,'Bilbo','Baggins'); True Log ${output} Should Be Equal As Strings ${output} None Verify person in first transaction - [Tags] db smoke Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True Begin second transaction - [Tags] db smoke ${output} = Execute SQL String SAVEPOINT second True Log ${output} Should Be Equal As Strings ${output} None Add person in second transaction - [Tags] db smoke ${output} = Execute SQL String INSERT INTO person VALUES(102,'Frodo','Baggins'); True Log ${output} Should Be Equal As Strings ${output} None Verify persons in first and second transactions - [Tags] db smoke Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 2 True Rollback second transaction - [Tags] db smoke ${output} = Execute SQL String ROLLBACK TO SAVEPOINT second True Log ${output} Should Be Equal As Strings ${output} None Verify second transaction rollback - [Tags] db smoke Row Count is Equal to X SELECT * FROM person WHERE last_name = 'Baggins'; 1 True Rollback first transaction - [Tags] db smoke ${output} = Execute SQL String ROLLBACK TO SAVEPOINT first True Log ${output} Should Be Equal As Strings ${output} None Verify first transaction rollback - [Tags] db smoke Row Count is 0 SELECT * FROM person WHERE last_name = 'Baggins'; True Drop person and foobar tables - [Tags] db smoke ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; Log ${output} Should Be Equal As Strings ${output} None + +Disconnect from all databases + Disconnect From All Databases diff --git a/test/tests/_old/PostgreSQL_DB_Tests.robot b/test/tests/_old/PostgreSQL_DB_Tests.robot index d77c106d..d0998202 100644 --- a/test/tests/_old/PostgreSQL_DB_Tests.robot +++ b/test/tests/_old/PostgreSQL_DB_Tests.robot @@ -4,6 +4,8 @@ Suite Teardown Disconnect From Database Library DatabaseLibrary Library OperatingSystem Library Collections +Test Tags main db smoke + *** Variables *** ${DBHost} localhost @@ -61,7 +63,6 @@ Retrieve records from person table Should Be Equal As Strings ${output} None Verify person Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT * FROM person LIMIT 1; Log Many @{queryResults} @@ -75,7 +76,6 @@ Verify person Description Should Be Equal As Integers ${NumColumns} 3 Verify foobar Description - [Tags] db smoke Comment Query db for table column descriptions @{queryResults} = Description SELECT * FROM foobar LIMIT 1; Log Many @{queryResults} @@ -103,7 +103,6 @@ Verify Query - Row Count foobar table Should be equal as Integers ${val} 0 Verify Query - Get results as a list of dictionaries - [Tags] db smoke ${output} = Query SELECT * FROM person; \ True Log ${output} Should Be Equal As Strings ${output}[0][first_name] Franz Allan @@ -146,3 +145,6 @@ Drop person and foobar tables ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; Log ${output} Should Be Equal As Strings ${output} None + +Disconnect from all databases + Disconnect From All Databases diff --git a/test/tests/_old/PyODBC_DB_Tests.robot b/test/tests/_old/PyODBC_DB_Tests.robot index 874b121e..a28cec07 100644 --- a/test/tests/_old/PyODBC_DB_Tests.robot +++ b/test/tests/_old/PyODBC_DB_Tests.robot @@ -4,6 +4,8 @@ Suite Teardown Disconnect From Database Library DatabaseLibrary Library Collections Library OperatingSystem +Test Tags main db smoke + *** Variables *** ${DBModule} pyodbc @@ -178,3 +180,6 @@ Drop person and foobar tables ${output} = Execute SQL String DROP TABLE IF EXISTS foobar; Log ${output} Should Be Equal As Strings ${output} None + +Disconnect from all databases + Disconnect From All Databases From 69dc9b16fb160381225cd715331b4be271229a5f Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Tue, 7 Nov 2023 21:23:50 +0100 Subject: [PATCH 02/15] Add tests for aliased connections --- test/Aliases_SQLite3_DB_Tests.robot | 55 -------- test/resources/common.resource | 9 +- .../common_tests/aliased_connection.robot | 129 ++++++++++++++++++ 3 files changed, 135 insertions(+), 58 deletions(-) delete mode 100644 test/Aliases_SQLite3_DB_Tests.robot create mode 100644 test/tests/common_tests/aliased_connection.robot diff --git a/test/Aliases_SQLite3_DB_Tests.robot b/test/Aliases_SQLite3_DB_Tests.robot deleted file mode 100644 index 40d7d725..00000000 --- a/test/Aliases_SQLite3_DB_Tests.robot +++ /dev/null @@ -1,55 +0,0 @@ -*** Settings *** -Library DatabaseLibrary -Library OperatingSystem -Force Tags main db smoke - -*** Variables *** -${DBName1} my_db_test1 -${DBName2} my_db_test2 - -*** Keywords *** -Remove DB file if exists - [Arguments] ${DB_FILE} - Run Keyword And Ignore Error Remove File ${DB_FILE} - File Should Not Exist ${DB_FILE} - Comment Sleep 1s - -*** Test Cases *** -Remove old DB if exists - Remove DB file if exists ${CURDIR}/${DBName1}.db - Remove DB file if exists ${CURDIR}/${DBName2}.db - -Connect to SQLiteDB - Comment Connect To Database Using Custom Params sqlite3 database='path_to_dbfile\dbname.db' - Connect To Database Using Custom Params sqlite3 database="${CURDIR}/${DBName1}.db", isolation_level=None alias=db1 - Connect To Database Using Custom Params sqlite3 database="${CURDIR}/${DBName2}.db", isolation_level=None alias=db2 - -Create person table - ${output} = Execute SQL String CREATE TABLE person (id integer unique,first_name varchar,last_name varchar); alias=db2 - Log ${output} - Should Be Equal As Strings ${output} None - -Create foobar table - ${output} = Execute SQL String create table foobar (id integer primary key, firstname varchar unique) alias=db1 - Log ${output} - Should Be Equal As Strings ${output} None - -Table Must Exist - person - Table Must Exist person alias=db2 - -Table Shouldn't Exist - person - Run Keyword And Expect Error Table 'person' does not exist in the db Table Must Exist person alias=db1 - -Table Shouldn't Exist - foobar - Run Keyword And Expect Error Table 'foobar' does not exist in the db Table Must Exist foobar alias=db2 - -Switch database without alias - Switch Database db2 - Table Must Exist person - Run Keyword And Expect Error Table 'foobar' does not exist in the db Table Must Exist foobar - -Disconnect from database db1 - Disconnect From Database alias=db1 - -Disconnect from all databases - Disconnect From All Databases diff --git a/test/resources/common.resource b/test/resources/common.resource index 6b78a1be..3856e19c 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -24,13 +24,14 @@ ${DB_DRIVER} ODBC Driver 18 for SQL Server Connect To DB [Documentation] Connects to the database based on the current DB module under test ... and connection params set in global variables + [Arguments] ${alias}=${None} IF "${DB_MODULE_MODE}" == "custom" IF "${DB_MODULE}" == "sqlite3" - Remove File ${DBName}.db - Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None + IF $alias is None Remove File ${DBName}.db + Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None alias=${alias} ELSE ${Connection String}= Build Connection String - Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} alias=${alias} END ELSE IF "${DB_MODULE_MODE}" == "standard" IF "${DB_MODULE}" == "pyodbc" @@ -42,6 +43,7 @@ Connect To DB ... ${DB_HOST} ... ${DB_PORT} ... dbDriver=${DB_DRIVER} + ... alias=${alias} ELSE Connect To Database ... ${DB_MODULE} @@ -50,6 +52,7 @@ Connect To DB ... ${DB_PASS} ... ${DB_HOST} ... ${DB_PORT} + ... alias=${alias} END ELSE Fail Unexpected mode - ${DB_MODULE_MODE} diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot new file mode 100644 index 00000000..29b89026 --- /dev/null +++ b/test/tests/common_tests/aliased_connection.robot @@ -0,0 +1,129 @@ +*** Settings *** +Resource ../../resources/common.resource +Suite Setup Run Keywords +... Connect To DB +... AND +... Create Person Table +Suite Teardown Run Keywords +... Connect To DB +... AND +... Drop Tables Person And Foobar +Test Teardown Disconnect From All Databases + + +*** Test Cases *** +Connections Can Be Aliased + Connect To DB # default alias + Connect To DB alias=second + +Default Alias Can Be Empty + Connect To DB # default alias + Query SELECT * FROM person + Connect To DB alias=second + Query SELECT * FROM person + Query SELECT * FROM person alias=second + +Switch From Default And Disconnect + Connect To DB # default alias + Connect To DB alias=second + Switch Database second + Query SELECT * FROM person # query with 'second' connection + Disconnect From Database alias=second + Query SELECT * FROM person # query with 'default' connection + +Disconnect Not Existing Alias + Connect To DB # default alias + Disconnect From Database alias=idontexist # silent warning + Run Keyword And Expect Error ConnectionError: No open database connection to close + ... Disconnect From Database alias=idontexist error_if_no_connection=${True} + # default alias exist and can be closed + Disconnect From Database error_if_no_connection=${True} + +Switch Not Existing Alias + Run Keyword And Expect Error ConnectionError: Non-existing index or alias 'second' + ... Switch Database second + +Execute SQL Script - Insert Data In Person table + Connect To DB alias=aliased_conn + ${output}= Execute SQL Script ${CURDIR}/../insert_data_in_person_table.sql alias=aliased_conn + Should Be Equal As Strings ${output} None + +Check If Exists In DB - Franz Allan + Connect To DB alias=aliased_conn + Check If Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Franz Allan' alias=aliased_conn + +Check If Not Exists In DB - Joe + Connect To DB alias=aliased_conn + Check If Not Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Joe' alias=aliased_conn + +Table Must Exist - person + Connect To DB alias=aliased_conn + Table Must Exist person alias=aliased_conn + +Verify Row Count is 0 + Connect To DB alias=aliased_conn + Row Count is 0 SELECT * FROM person WHERE FIRST_NAME= 'NotHere' alias=aliased_conn + +Verify Row Count is Equal to X + Connect To DB alias=aliased_conn + Row Count is Equal to X SELECT id FROM person 2 alias=aliased_conn + +Verify Row Count is Less Than X + Connect To DB alias=aliased_conn + Row Count is Less Than X SELECT id FROM person 3 alias=aliased_conn + +Verify Row Count is Greater Than X + Connect To DB alias=aliased_conn + Row Count is Greater Than X SELECT * FROM person 1 alias=aliased_conn + +Retrieve Row Count + Connect To DB alias=aliased_conn + ${output}= Row Count SELECT id FROM person alias=aliased_conn + Log ${output} + Should Be Equal As Strings ${output} 2 + +Retrieve records from person table + Connect To DB alias=aliased_conn + ${output}= Execute SQL String SELECT * FROM person + Log ${output} + Should Be Equal As Strings ${output} None + +Verify Query - Row Count person table + ${output}= Query SELECT COUNT(*) FROM person + Log ${output} + Should Be Equal As Integers ${output}[0][0] 2 + +Verify Query - Get results as a list of dictionaries + Connect To DB alias=aliased_conn + ${output}= Query SELECT * FROM person returnAsDict=True alias=aliased_conn + Log ${output} + # some databases lower field names and you can't do anything about it + TRY + ${value 1}= Get From Dictionary ${output}[0] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 1}= Get From Dictionary ${output}[0] first_name + END + TRY + ${value 2}= Get From Dictionary ${output}[1] FIRST_NAME + EXCEPT Dictionary does not contain key 'FIRST_NAME'. + ${value 2}= Get From Dictionary ${output}[1] first_name + END + Should Be Equal As Strings ${value 1} Franz Allan + Should Be Equal As Strings ${value 2} Jerry + +Verify Delete All Rows From Table + Connect To DB alias=aliased_conn + Delete All Rows From Table person alias=aliased_conn + Row Count Is 0 SELECT * FROM foobar alias=aliased_conn + + + +*** Keywords *** +Aliases Suite Setup + Connect To DB + Create Person Table + +Aliases Suite Teardown + Connect To DB + Drop Tables Person And Foobar + Disconnect From All Databases From 69c31434d084e8f5d65a06a3a675b80e87c1cfea Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Wed, 8 Nov 2023 10:58:02 +0100 Subject: [PATCH 03/15] Redesign connection cache to clear connection after disconnect --- src/DatabaseLibrary/__init__.py | 2 +- src/DatabaseLibrary/assertion.py | 47 +++++++--- src/DatabaseLibrary/connection_manager.py | 86 +++++++++++++------ src/DatabaseLibrary/query.py | 82 +++++++++--------- test/resources/common.resource | 43 ++++++++-- .../common_tests/aliased_connection.robot | 60 ++++++------- 6 files changed, 205 insertions(+), 115 deletions(-) diff --git a/src/DatabaseLibrary/__init__.py b/src/DatabaseLibrary/__init__.py index efb757f1..cd70e0b4 100644 --- a/src/DatabaseLibrary/__init__.py +++ b/src/DatabaseLibrary/__init__.py @@ -69,7 +69,7 @@ class DatabaseLibrary(ConnectionManager, Query, Assertion): The library is basically compatible with any [https://peps.python.org/pep-0249|Python Database API Specification 2.0] module. However, the actual implementation in existing Python modules is sometimes quite different, which requires custom handling in the library. - Therefore there are some modules, which are "natively" supported in the library - and others, which may work and may not. + Therefore, there are some modules, which are "natively" supported in the library - and others, which may work and may not. See more on the [https://github.com/MarketSquare/Robotframework-Database-Library|project page on GitHub]. """ diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index ea586943..50c3c194 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -21,7 +21,9 @@ class Assertion: Assertion handles all the assertions of Database Library. """ - def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): + def check_if_exists_in_database( + self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + ): """ Check if any row would be returned by given the input `selectStatement`. If there are no results, then this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction @@ -52,7 +54,9 @@ def check_if_exists_in_database(self, selectStatement: str, sansTran: bool = Fal msg or f"Expected to have have at least one row, but got 0 rows from: '{selectStatement}'" ) - def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): + def check_if_not_exists_in_database( + self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + ): """ This is the negation of `check_if_exists_in_database`. @@ -86,7 +90,9 @@ def check_if_not_exists_in_database(self, selectStatement: str, sansTran: bool = msg or f"Expected to have have no rows from '{selectStatement}', but got some rows: {query_results}" ) - def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): + def row_count_is_0( + self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + ): """ Check if any rows are returned from the submitted `selectStatement`. If there are, then this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction commit or @@ -117,7 +123,12 @@ def row_count_is_0(self, selectStatement: str, sansTran: bool = False, msg: Opti raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'") def row_count_is_equal_to_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: str = "default", ): """ Check if the number of rows returned from `selectStatement` is equal to the value submitted. If not, then this @@ -152,7 +163,12 @@ def row_count_is_equal_to_x( ) def row_count_is_greater_than_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: str = "default", ): """ Check if the number of rows returned from `selectStatement` is greater than the value submitted. If not, then @@ -187,7 +203,12 @@ def row_count_is_greater_than_x( ) def row_count_is_less_than_x( - self, selectStatement: str, numRows: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + self, + selectStatement: str, + numRows: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: str = "default", ): """ Check if the number of rows returned from `selectStatement` is less than the value submitted. If not, then this @@ -221,7 +242,9 @@ def row_count_is_less_than_x( msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" ) - def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None): + def table_must_exist( + self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + ): """ Check if the table given exists in the database. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. The default error message can be overridden with the `msg` argument. @@ -243,20 +266,20 @@ def table_must_exist(self, tableName: str, sansTran: bool = False, msg: Optional | Table Must Exist | first_name | msg=my error message | """ logger.info(f"Executing : Table Must Exist | {tableName}") - _, db_api_module_name = self._cache.switch(alias) - if db_api_module_name in ["cx_Oracle", "oracledb"]: + db_connection = self._get_connection_with_alias(alias) + if db_connection.module_name in ["cx_Oracle", "oracledb"]: query = ( "SELECT * FROM all_objects WHERE object_type IN ('TABLE','VIEW') AND " f"owner = SYS_CONTEXT('USERENV', 'SESSION_USER') AND object_name = UPPER('{tableName}')" ) table_exists = self.row_count(query, sansTran, alias=alias) > 0 - elif db_api_module_name in ["sqlite3"]: + elif db_connection.module_name in ["sqlite3"]: query = f"SELECT name FROM sqlite_master WHERE type='table' AND name='{tableName}' COLLATE NOCASE" table_exists = self.row_count(query, sansTran, alias=alias) > 0 - elif db_api_module_name in ["ibm_db", "ibm_db_dbi"]: + elif db_connection.module_name in ["ibm_db", "ibm_db_dbi"]: query = f"SELECT name FROM SYSIBM.SYSTABLES WHERE type='T' AND name=UPPER('{tableName}')" table_exists = self.row_count(query, sansTran, alias=alias) > 0 - elif db_api_module_name in ["teradata"]: + elif db_connection.module_name in ["teradata"]: query = f"SELECT TableName FROM DBC.TablesV WHERE TableKind='T' AND TableName='{tableName}'" table_exists = self.row_count(query, sansTran, alias=alias) > 0 else: diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 13259b05..2561cc83 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -13,7 +13,8 @@ # limitations under the License. import importlib -from typing import Optional +from dataclasses import dataclass +from typing import Any, Dict, Optional try: import ConfigParser @@ -21,7 +22,12 @@ import configparser as ConfigParser from robot.api import logger -from robot.utils import ConnectionCache + + +@dataclass +class Connection: + client: Any + module_name: str class ConnectionManager: @@ -30,8 +36,14 @@ class ConnectionManager: """ def __init__(self): - self.omit_trailing_semicolon = False - self._cache = ConnectionCache("No sessions created") + self.omit_trailing_semicolon: bool = False + self._connections: Dict[str, Connection] = {} + self.default_alias: str = "default" + + def _register_connection(self, client: Any, module_name: str, alias: str): + if alias in self._connections: + logger.warn(f"Overwriting not closed connection for alias = '{alias}'") + self._connections[alias] = Connection(client, module_name) def connect_to_database( self, @@ -45,11 +57,14 @@ def connect_to_database( dbDriver: Optional[str] = None, dbConfigFile: Optional[str] = None, driverMode: Optional[str] = None, - alias: Optional[str] = "default", + alias: str = "default", ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to - connect to the database using `dbName`, `dbUsername`, and `dbPassword`. + connect to the database using provided parameters such as `dbName`, `dbUsername`, and `dbPassword`. + + Optional ``alias`` parameter can be used for creating multiple open connections, even for different databases. + If the same alias is given twice then previous connection will be overriden. The `driverMode` is used to select the *oracledb* client mode. Allowed values are: @@ -261,10 +276,10 @@ def connect_to_database( host=dbHost, port=dbPort, ) - self._cache.register((db_connection, db_api_module_name), alias=alias) + self._register_connection(db_connection, db_api_module_name, alias) def connect_to_database_using_custom_params( - self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: Optional[str] = "default" + self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: str = "default" ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to @@ -299,10 +314,10 @@ def connect_to_database_using_custom_params( ) db_connection = eval(db_connect_string) - self._cache.register((db_connection, db_api_module_name), alias=alias) + self._register_connection(db_connection, db_api_module_name, alias) def connect_to_database_using_custom_connection_string( - self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: Optional[str] = "default" + self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: str = "default" ): """ Loads the DB API 2.0 module given `dbapiModuleName` then uses it to @@ -323,9 +338,9 @@ def connect_to_database_using_custom_connection_string( f"'{db_connect_string}')" ) db_connection = db_api_2.connect(db_connect_string) - self._cache.register((db_connection, db_api_module_name), alias=alias) + self._register_connection(db_connection, db_api_module_name, alias) - def disconnect_from_database(self, error_if_no_connection: bool = False, alias: Optional[str] = "default"): + def disconnect_from_database(self, error_if_no_connection: bool = False, alias: Optional[str] = None): """ Disconnects from the database. @@ -338,13 +353,12 @@ def disconnect_from_database(self, error_if_no_connection: bool = False, alias: | Disconnect From Database | alias=my_alias | # disconnects from current connection to the database | """ logger.info("Executing : Disconnect From Database") + if not alias: + alias = self.default_alias try: - db_connection, _ = self._cache.switch(alias) - except RuntimeError: # Non-existing index or alias - db_connection = None - if db_connection: - db_connection.close() - else: + db_connection = self._connections.pop(alias) + db_connection.client.close() + except KeyError: # Non-existing alias log_msg = "No open database connection to close" if error_if_no_connection: raise ConnectionError(log_msg) from None @@ -358,12 +372,11 @@ def disconnect_from_all_databases(self): | Disconnect From All Databases | # disconnects from all connections to the database | """ logger.info("Executing : Disconnect From All Databases") - for db_connection, _ in self._cache: - if db_connection: - db_connection.close() - self._cache.empty_cache() + for db_connection in self._connections.values(): + db_connection.client.close() + self._connections = {} - def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = "default"): + def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = None): """ Turn the autocommit on the database connection ON or OFF. @@ -381,10 +394,10 @@ def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = "defau | Set Auto Commit | False """ logger.info("Executing : Set Auto Commit") - db_connection, _ = self._cache.switch(alias) - db_connection.autocommit = autoCommit + db_connection = self._get_connection_with_alias(alias) + db_connection.client.autocommit = autoCommit - def switch_database(self, alias): + def switch_database(self, alias: str): """ Switch default database. @@ -392,4 +405,23 @@ def switch_database(self, alias): | Switch Database | my_alias | | Switch Database | alias=my_alias | """ - self._cache.switch(alias) + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + self.default_alias = alias + + def _get_connection_with_alias(self, alias: Optional[str]) -> Connection: + """ + Return connection with given alias. + + If alias is not provided, it will return default connection. + If there is no default connection, it will return last opened connection. + """ + if not self._connections: + raise ValueError(f"No database connection is open.") + if not alias: + if self.default_alias in self._connections: + return self._connections[self.default_alias] + return list(self._connections.values())[-1] + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + return self._connections[alias] diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index a59ba536..899cd751 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -24,7 +24,9 @@ class Query: Query handles all the querying done by the Database Library. """ - def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None): + def query( + self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None + ): """ Uses the input `selectStatement` to query for the values that will be returned as a list of tuples. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. @@ -59,10 +61,10 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Query | SELECT * FROM person | True | """ - db_connection, _ = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) cur = None try: - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Query | {selectStatement} ") self.__execute_sql(cur, selectStatement) all_rows = cur.fetchall() @@ -72,7 +74,7 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool return all_rows finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ @@ -102,19 +104,19 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona Using optional `sansTran` to run command without an explicit transaction commit or rollback: | ${rowCount} | Row Count | SELECT * FROM person | True | """ - db_connection, db_api_module_name = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) cur = None try: - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Row Count | {selectStatement}") self.__execute_sql(cur, selectStatement) data = cur.fetchall() - if db_api_module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: + if db_connection.module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: return len(data) return cur.rowcount finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ @@ -138,10 +140,10 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Description | SELECT * FROM person | True | """ - db_connection, _ = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) cur = None try: - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info("Executing : Description | {selectStatement}") self.__execute_sql(cur, selectStatement) description = list(cur.description) @@ -151,7 +153,7 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio return description finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, alias: Optional[str] = None): """ @@ -173,22 +175,22 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Delete All Rows From Table | person | True | """ - db_connection, _ = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) cur = None query = f"DELETE FROM {tableName}" try: - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Delete All Rows From Table | {query}") result = self.__execute_sql(cur, query) if result is not None: if not sansTran: - db_connection.commit() + db_connection.client.commit() return result if not sansTran: - db_connection.commit() + db_connection.client.commit() finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, alias: Optional[str] = None): """ @@ -249,12 +251,12 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | True | """ - db_connection, _ = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) with open(sqlScriptFileName, encoding="UTF-8") as sql_file: cur = None try: statements_to_execute = [] - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Execute SQL Script | {sqlScriptFileName}") current_statement = "" inside_statements_group = False @@ -310,10 +312,10 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali omit_semicolon = not statement.lower().endswith("end;") self.__execute_sql(cur, statement, omit_semicolon) if not sansTran: - db_connection.commit() + db_connection.client.commit() finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None): """ @@ -332,19 +334,21 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Opti Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | True | """ - db_connection, _ = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) cur = None try: - cur = db_connection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Execute SQL String | {sqlString}") self.__execute_sql(cur, sqlString) if not sansTran: - db_connection.commit() + db_connection.client.commit() finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() - def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = None, sansTran: bool = False, alias: Optional[str] = None): + def call_stored_procedure( + self, spName: str, spParams: Optional[List[str]] = None, sansTran: bool = False, alias: Optional[str] = None + ): """ Calls a stored procedure `spName` with the `spParams` - a *list* of parameters the procedure requires. Use the special *CURSOR* value for OUT params, which should receive result sets - @@ -383,21 +387,21 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{Param values} @{Result sets} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | ${Params} | True | """ - db_connection, db_api_module_name = self._cache.switch(alias) + db_connection = self._get_connection_with_alias(alias) if spParams is None: spParams = [] cur = None try: logger.info(f"Executing : Call Stored Procedure | {spName} | {spParams}") - if db_api_module_name == "pymssql": - cur = db_connection.cursor(as_dict=False) + if db_connection.module_name == "pymssql": + cur = db_connection.client.cursor(as_dict=False) else: - cur = db_connection.cursor() + cur = db_connection.client.cursor() param_values = [] result_sets = [] - if db_api_module_name == "pymysql": + if db_connection.module_name == "pymysql": cur.callproc(spName, spParams) # first proceed the result sets if available @@ -414,22 +418,22 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non cur.execute(f"select @_{spName}_{i}") param_values.append(cur.fetchall()[0][0]) - elif db_api_module_name in ["oracledb", "cx_Oracle"]: + elif db_connection.module_name in ["oracledb", "cx_Oracle"]: # check if "CURSOR" params were passed - they will be replaced # with cursor variables for storing the result sets params_substituted = spParams.copy() cursor_params = [] for i in range(0, len(spParams)): if spParams[i] == "CURSOR": - cursor_param = db_connection.cursor() + cursor_param = db_connection.client.cursor() params_substituted[i] = cursor_param cursor_params.append(cursor_param) param_values = cur.callproc(spName, params_substituted) for result_set in cursor_params: result_sets.append(list(result_set)) - elif db_api_module_name in ["psycopg2", "psycopg3"]: - cur = db_connection.cursor() + elif db_connection.module_name in ["psycopg2", "psycopg3"]: + cur = db_connection.client.cursor() # check if "CURSOR" params were passed - they will be replaced # with cursor variables for storing the result sets params_substituted = spParams.copy() @@ -446,7 +450,7 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non result_set = cur.fetchall() result_sets.append(list(result_set)) else: - if db_api_module_name in ["psycopg3"]: + if db_connection.module_name in ["psycopg3"]: result_sets_available = True while result_sets_available: result_sets.append(list(cur.fetchall())) @@ -457,10 +461,10 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non else: logger.info( - f"CAUTION! Calling a stored procedure for '{db_api_module_name}' is not tested, " + f"CAUTION! Calling a stored procedure for '{db_connection.module_name}' is not tested, " "results might be invalid!" ) - cur = db_connection.cursor() + cur = db_connection.client.cursor() param_values = cur.callproc(spName, spParams) logger.info("Reading the procedure results..") result_sets_available = True @@ -476,12 +480,12 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non result_sets_available = False if not sansTran: - db_connection.commit() + db_connection.client.commit() return param_values, result_sets finally: if cur and not sansTran: - db_connection.rollback() + db_connection.client.rollback() def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Optional[bool] = None): """ diff --git a/test/resources/common.resource b/test/resources/common.resource index 3856e19c..1acf6e31 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -24,14 +24,13 @@ ${DB_DRIVER} ODBC Driver 18 for SQL Server Connect To DB [Documentation] Connects to the database based on the current DB module under test ... and connection params set in global variables - [Arguments] ${alias}=${None} IF "${DB_MODULE_MODE}" == "custom" IF "${DB_MODULE}" == "sqlite3" - IF $alias is None Remove File ${DBName}.db - Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None alias=${alias} + Remove File ${DBName}.db + Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None ELSE ${Connection String}= Build Connection String - Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} alias=${alias} + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} END ELSE IF "${DB_MODULE_MODE}" == "standard" IF "${DB_MODULE}" == "pyodbc" @@ -43,7 +42,6 @@ Connect To DB ... ${DB_HOST} ... ${DB_PORT} ... dbDriver=${DB_DRIVER} - ... alias=${alias} ELSE Connect To Database ... ${DB_MODULE} @@ -52,12 +50,40 @@ Connect To DB ... ${DB_PASS} ... ${DB_HOST} ... ${DB_PORT} - ... alias=${alias} END ELSE Fail Unexpected mode - ${DB_MODULE_MODE} END +Connect To DB Aliased + [Documentation] Connects to the database based on the current DB module under test + ... and connection params set in global variables with alias + [Arguments] ${alias}=${None} + ${DB_KWARGS} Create Dictionary + IF $alias is not None Set To Dictionary ${DB_KWARGS} alias=${alias} + IF "${DB_MODULE_MODE}" == "custom" + IF "${DB_MODULE}" == "sqlite3" + IF $alias is None + # Remove File ${DBName}.db TODO Figure out when clear db for aliased tests + Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None + ELSE + Connect To Database Using Custom Params + ... sqlite3 + ... database="./${DBName}.db", isolation_level=None + ... alias=${alias} + END + ELSE + ${Connection String}= Build Connection String + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} &{DB_KWARGS} + END + ELSE IF "${DB_MODULE_MODE}" == "standard" + ${DB_ARGS} Create List ${DB_MODULE} ${DB_NAME} ${DB_USER} ${DB_PASS} ${DB_HOST} ${DB_PORT} + IF "${DB_MODULE}" == "pyodbc" Set To Dictionary ${DB_KWARGS} dbDriver=${DB_DRIVER} + Connect To Database @{DB_ARGS} &{DB_KWARGS} + ELSE + Fail Unexpected mode - ${DB_MODULE_MODE} + END + Build Connection String [Documentation] Returns the connection string variable depending on the DB module ... currently under test. @@ -87,6 +113,11 @@ Insert Data In Person Table Using SQL Script ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql RETURN ${output} +Insert Data In Person Table Using SQL Script Aliased + [Arguments] ${alias} + ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql alias=${alias} + RETURN ${output} + Create Foobar Table ${sql}= Catenate ... CREATE TABLE foobar diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot index 29b89026..1beabbfa 100644 --- a/test/tests/common_tests/aliased_connection.robot +++ b/test/tests/common_tests/aliased_connection.robot @@ -13,26 +13,26 @@ Test Teardown Disconnect From All Databases *** Test Cases *** Connections Can Be Aliased - Connect To DB # default alias - Connect To DB alias=second + Connect To DB Aliased # default alias + Connect To DB Aliased alias=second Default Alias Can Be Empty - Connect To DB # default alias + Connect To DB Aliased # default alias Query SELECT * FROM person - Connect To DB alias=second + Connect To DB Aliased alias=second Query SELECT * FROM person Query SELECT * FROM person alias=second Switch From Default And Disconnect - Connect To DB # default alias - Connect To DB alias=second + Connect To DB Aliased # default alias + Connect To DB Aliased alias=second Switch Database second Query SELECT * FROM person # query with 'second' connection Disconnect From Database alias=second Query SELECT * FROM person # query with 'default' connection Disconnect Not Existing Alias - Connect To DB # default alias + Connect To DB Aliased # default alias Disconnect From Database alias=idontexist # silent warning Run Keyword And Expect Error ConnectionError: No open database connection to close ... Disconnect From Database alias=idontexist error_if_no_connection=${True} @@ -40,62 +40,63 @@ Disconnect Not Existing Alias Disconnect From Database error_if_no_connection=${True} Switch Not Existing Alias - Run Keyword And Expect Error ConnectionError: Non-existing index or alias 'second' + Run Keyword And Expect Error ValueError: Alias 'second' not found in existing connections. ... Switch Database second Execute SQL Script - Insert Data In Person table - Connect To DB alias=aliased_conn - ${output}= Execute SQL Script ${CURDIR}/../insert_data_in_person_table.sql alias=aliased_conn + Connect To DB Aliased alias=aliased_conn + ${output} Insert Data In Person Table Using SQL Script Aliased alias=aliased_conn Should Be Equal As Strings ${output} None Check If Exists In DB - Franz Allan - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Check If Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Franz Allan' alias=aliased_conn Check If Not Exists In DB - Joe - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Check If Not Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Joe' alias=aliased_conn Table Must Exist - person - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Table Must Exist person alias=aliased_conn Verify Row Count is 0 - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Row Count is 0 SELECT * FROM person WHERE FIRST_NAME= 'NotHere' alias=aliased_conn Verify Row Count is Equal to X - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Row Count is Equal to X SELECT id FROM person 2 alias=aliased_conn Verify Row Count is Less Than X - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Row Count is Less Than X SELECT id FROM person 3 alias=aliased_conn Verify Row Count is Greater Than X - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Row Count is Greater Than X SELECT * FROM person 1 alias=aliased_conn Retrieve Row Count - Connect To DB alias=aliased_conn - ${output}= Row Count SELECT id FROM person alias=aliased_conn + Connect To DB Aliased alias=aliased_conn + ${output} Row Count SELECT id FROM person alias=aliased_conn Log ${output} Should Be Equal As Strings ${output} 2 Retrieve records from person table - Connect To DB alias=aliased_conn - ${output}= Execute SQL String SELECT * FROM person + Connect To DB Aliased alias=aliased_conn + ${output} Execute SQL String SELECT * FROM person Log ${output} Should Be Equal As Strings ${output} None -Verify Query - Row Count person table - ${output}= Query SELECT COUNT(*) FROM person +Use Last Connected If Not Alias Provided + Connect To DB Aliased alias=aliased_conn + ${output} Query SELECT COUNT(*) FROM person Log ${output} Should Be Equal As Integers ${output}[0][0] 2 Verify Query - Get results as a list of dictionaries - Connect To DB alias=aliased_conn - ${output}= Query SELECT * FROM person returnAsDict=True alias=aliased_conn + Connect To DB Aliased alias=aliased_conn + ${output} Query SELECT * FROM person returnAsDict=True alias=aliased_conn Log ${output} # some databases lower field names and you can't do anything about it TRY @@ -112,18 +113,17 @@ Verify Query - Get results as a list of dictionaries Should Be Equal As Strings ${value 2} Jerry Verify Delete All Rows From Table - Connect To DB alias=aliased_conn + Connect To DB Aliased alias=aliased_conn Delete All Rows From Table person alias=aliased_conn - Row Count Is 0 SELECT * FROM foobar alias=aliased_conn - + Row Count Is 0 SELECT * FROM person alias=aliased_conn *** Keywords *** Aliases Suite Setup - Connect To DB + Connect To DB Aliased Create Person Table Aliases Suite Teardown - Connect To DB + Connect To DB Aliased Drop Tables Person And Foobar Disconnect From All Databases From d619a925fd9cdaa6fbfd1a6df23846aa76165f33 Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Thu, 9 Nov 2023 14:16:00 +0100 Subject: [PATCH 04/15] Update documentation with aliases and improved display --- src/DatabaseLibrary/assertion.py | 49 ++++++++++++++++------- src/DatabaseLibrary/connection_manager.py | 12 +++--- src/DatabaseLibrary/query.py | 40 +++++++++++++----- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 50c3c194..de091117 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -25,9 +25,9 @@ def check_if_exists_in_database( self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ - Check if any row would be returned by given the input `selectStatement`. If there are no results, then this will - throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction - commit or rollback. The default error message can be overridden with the `msg` argument. + Check if any row would be returned by given the input ``selectStatement``. If there are no results, then this will + throw an AssertionError. Set optional input ``sansTran`` to True to run command without an explicit transaction + commit or rollback. The default error message can be overridden with the ``msg`` argument. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -42,10 +42,13 @@ def check_if_exists_in_database( | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # PASS | | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # FAIL | - Using optional `sansTran` to run command without an explicit transaction commit or rollback: + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | - Using optional `msg` to override the default error message: + Using optional ``msg`` to override the default error message: | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | msg=my error message | """ logger.info(f"Executing : Check If Exists In Database | {selectStatement}") @@ -60,9 +63,9 @@ def check_if_not_exists_in_database( """ This is the negation of `check_if_exists_in_database`. - Check if no rows would be returned by given the input `selectStatement`. If there are any results, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. The default error message can be overridden with the `msg` argument. + Check if no rows would be returned by given the input ``selectStatement``. If there are any results, then this + will throw an AssertionError. Set optional input ``sansTran`` to True to run command without an explicit + transaction commit or rollback. The default error message can be overridden with the ``msg`` argument. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -77,10 +80,13 @@ def check_if_not_exists_in_database( | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # PASS | | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | - Using optional `sansTran` to run command without an explicit transaction commit or rollback: + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | - Using optional `msg` to override the default error message: + Using optional ``msg`` to override the default error message: | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | """ logger.info(f"Executing : Check If Not Exists In Database | {selectStatement}") @@ -94,9 +100,9 @@ def row_count_is_0( self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ - Check if any rows are returned from the submitted `selectStatement`. If there are, then this will throw an - AssertionError. Set optional input `sansTran` to True to run command without an explicit transaction commit or - rollback. The default error message can be overridden with the `msg` argument. + Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an + AssertionError. Set optional input ``sansTran`` to True to run command without an explicit transaction commit or + rollback. The default error message can be overridden with the ``msg`` argument. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -111,7 +117,10 @@ def row_count_is_0( | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | # PASS | - Using optional `sansTran` to run command without an explicit transaction commit or rollback: + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | True | Using optional `msg` to override the default error message: @@ -149,6 +158,9 @@ def row_count_is_equal_to_x( | Row Count Is Equal To X | SELECT id FROM person | 1 | # FAIL | | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | # PASS | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | True | @@ -189,6 +201,9 @@ def row_count_is_greater_than_x( | Row Count Is Greater Than X | SELECT id FROM person | 1 | # PASS | | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | # FAIL | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Row Count Is Greater Than X | SELECT id FROM person | 1 | True | @@ -229,6 +244,9 @@ def row_count_is_less_than_x( | Row Count Is Less Than X | SELECT id FROM person | 3 | # PASS | | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | # FAIL | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Row Count Is Less Than X | SELECT id FROM person | 3 | True | @@ -259,6 +277,9 @@ def table_must_exist( | Table Must Exist | first_name | # FAIL | | Table Must Exist | first_name | alias=my_alias | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Table Must Exist | person | True | diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 2561cc83..d2d2699a 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -60,21 +60,21 @@ def connect_to_database( alias: str = "default", ): """ - Loads the DB API 2.0 module given `dbapiModuleName` then uses it to - connect to the database using provided parameters such as `dbName`, `dbUsername`, and `dbPassword`. + Loads the DB API 2.0 module given ``dbapiModuleName`` then uses it to + connect to the database using provided parameters such as ``dbName``, ``dbUsername``, and ``dbPassword``. Optional ``alias`` parameter can be used for creating multiple open connections, even for different databases. If the same alias is given twice then previous connection will be overriden. - The `driverMode` is used to select the *oracledb* client mode. + The ``driverMode`` is used to select the *oracledb* client mode. Allowed values are: - _thin_ (default if omitted) - _thick_ - _thick,lib_dir=_ - Optionally, you can specify a `dbConfigFile` wherein it will load the - alias (or alias will be "default") property values for `dbapiModuleName`, `dbName` `dbUsername` - and `dbPassword` (note: specifying `dbapiModuleName`, `dbName` + Optionally, you can specify a ``dbConfigFile`` wherein it will load the + alias (or alias will be "default") property values for ``dbapiModuleName``, ``dbName`` ``dbUsername`` + and ``dbPassword`` (note: specifying ``dbapiModuleName``, ``dbName`` `dbUsername` or `dbPassword` directly will override the properties of the same key in `dbConfigFile`). If no `dbConfigFile` is specified, it defaults to `./resources/db.cfg`. diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index 899cd751..e5b6cb45 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -28,9 +28,12 @@ def query( self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None ): """ - Uses the input `selectStatement` to query for the values that will be returned as a list of tuples. Set optional - input `sansTran` to True to run command without an explicit transaction commit or rollback. - Set optional input `returnAsDict` to True to return values as a list of dictionaries. + Uses the input ``selectStatement`` to query for the values that will be returned as a list of tuples. Set + optional input ``sansTran`` to True to run command without an explicit transaction commit or rollback. + Set optional input ``returnAsDict`` to True to return values as a list of dictionaries. + + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. Tip: Unless you want to log all column values of the specified rows, try specifying the column names in your select statements @@ -58,7 +61,7 @@ def query( And get the following See, Franz Allan - Using optional `sansTran` to run command without an explicit transaction commit or rollback: + Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | @{queryResults} | Query | SELECT * FROM person | True | """ db_connection = self._get_connection_with_alias(alias) @@ -78,8 +81,8 @@ def query( def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ - Uses the input `selectStatement` to query the database and returns the number of rows from the query. Set - optional input `sansTran` to True to run command without an explicit transaction commit or rollback. + Uses the input ``selectStatement`` to query the database and returns the number of rows from the query. Set + optional input ``sansTran`` to True to run command without an explicit transaction commit or rollback. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -101,7 +104,10 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona And get the following 1 - Using optional `sansTran` to run command without an explicit transaction commit or rollback: + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + + Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | ${rowCount} | Row Count | SELECT * FROM person | True | """ db_connection = self._get_connection_with_alias(alias) @@ -120,8 +126,8 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): """ - Uses the input `selectStatement` to query a table in the db which will be used to determine the description. Set - optional input `sansTran` to True to run command without an explicit transaction commit or rollback. + Uses the input ``selectStatement`` to query a table in the db which will be used to determine the description. Set + optional input ``sansTran` to True to run command without an explicit transaction commit or rollback. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -137,6 +143,9 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio [Column(name='first_name', type_code=1043, display_size=None, internal_size=255, precision=None, scale=None, null_ok=None)] [Column(name='last_name', type_code=1043, display_size=None, internal_size=255, precision=None, scale=None, null_ok=None)] + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Description | SELECT * FROM person | True | """ @@ -172,6 +181,9 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali will get: | Delete All Rows From Table | first_name | # FAIL | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Delete All Rows From Table | person | True | """ @@ -198,7 +210,6 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali state before running your tests, or clearing out your test data after running each a test. Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. - Sample usage : | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-setup.sql | @@ -248,6 +259,9 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali The slash signs ("/") are always ignored and have no impact on execution order. + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | True | """ @@ -331,6 +345,9 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Opti For example with an argument: | Execute Sql String | SELECT * FROM person WHERE first_name = ${FIRSTNAME} | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | True | """ @@ -384,6 +401,9 @@ def call_stored_procedure( | # ${Param values} = [>, >] | | # ${result sets} = [[('Franz Allan',), ('Jerry',)], [('See',), ('Schneider',)]] | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{Param values} @{Result sets} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | ${Params} | True | """ From 32722b194bf2babec826be4426ecfa3d54a7bf5f Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Thu, 9 Nov 2023 14:27:11 +0100 Subject: [PATCH 05/15] Update connection setup & teardown in tests --- run_tests.sh | 30 ------------------ src/DatabaseLibrary/assertion.py | 6 ++-- src/DatabaseLibrary/connection_manager.py | 10 ++++-- test/resources/common.resource | 10 ++++++ test/tests/_old/DB2SQL_DB_Tests.robot | 31 ++++++------------- test/tests/_old/MySQL_DB_Tests.robot | 3 -- test/tests/_old/PostgreSQL_DB_Tests.robot | 3 -- .../common_tests/aliased_connection.robot | 21 ++----------- 8 files changed, 32 insertions(+), 82 deletions(-) delete mode 100755 run_tests.sh diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 8d7caf40..00000000 --- a/run_tests.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -xe - -function startup { - docker-compose up -d - sleep 10 -} - -function cleanup { - docker-compose down -} - -if [[ $1 == "clean" ]] -then - trap cleanup EXIT - startup - sleep 10 -fi - -export MYSQL_PORT=`docker-compose port mysqldb 3306 | cut -d ":" -f 2` -export POSTGRESQL_PORT=`docker-compose port postgresqldb 5432 | cut -d ":" -f 2` -export DB2_PORT=`docker-compose port db2db 50000 | cut -d ":" -f 2` - -yq e -i ' - .MYSQL_DBPort = env(MYSQL_PORT) | - .POSTGRESQL_DBPort = env(POSTGRESQL_PORT) | - .DB2_DBPort = env(DB2_PORT) -' test/DB_Variables.yaml - - -robot --randomize none -V test/DB_Variables.yaml -i main test diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index de091117..104a85bb 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -137,7 +137,7 @@ def row_count_is_equal_to_x( numRows: str, sansTran: bool = False, msg: Optional[str] = None, - alias: str = "default", + alias: Optional[str] = None, ): """ Check if the number of rows returned from `selectStatement` is equal to the value submitted. If not, then this @@ -180,7 +180,7 @@ def row_count_is_greater_than_x( numRows: str, sansTran: bool = False, msg: Optional[str] = None, - alias: str = "default", + alias: Optional[str] = None, ): """ Check if the number of rows returned from `selectStatement` is greater than the value submitted. If not, then @@ -223,7 +223,7 @@ def row_count_is_less_than_x( numRows: str, sansTran: bool = False, msg: Optional[str] = None, - alias: str = "default", + alias: Optional[str] = None, ): """ Check if the number of rows returned from `selectStatement` is less than the value submitted. If not, then this diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index d2d2699a..964b2141 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -42,7 +42,10 @@ def __init__(self): def _register_connection(self, client: Any, module_name: str, alias: str): if alias in self._connections: - logger.warn(f"Overwriting not closed connection for alias = '{alias}'") + if alias == self.default_alias: + logger.warn("Overwriting not closed connection.") + else: + logger.warn(f"Overwriting not closed connection for alias = '{alias}'") self._connections[alias] = Connection(client, module_name) def connect_to_database( @@ -354,7 +357,10 @@ def disconnect_from_database(self, error_if_no_connection: bool = False, alias: """ logger.info("Executing : Disconnect From Database") if not alias: - alias = self.default_alias + if not self._connections or self.default_alias in self._connections: + alias = self.default_alias + else: + alias = list(self._connections.keys())[-1] try: db_connection = self._connections.pop(alias) db_connection.client.close() diff --git a/test/resources/common.resource b/test/resources/common.resource index 1acf6e31..9ae0764f 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -84,6 +84,16 @@ Connect To DB Aliased Fail Unexpected mode - ${DB_MODULE_MODE} END +Aliased Suite Setup + Connect To DB + Create Person Table + Disconnect From Database + +Aliased Suite Teardown + Connect To DB + Drop Tables Person And Foobar + Disconnect From Database + Build Connection String [Documentation] Returns the connection string variable depending on the DB module ... currently under test. diff --git a/test/tests/_old/DB2SQL_DB_Tests.robot b/test/tests/_old/DB2SQL_DB_Tests.robot index e3e4d487..a66bdaac 100644 --- a/test/tests/_old/DB2SQL_DB_Tests.robot +++ b/test/tests/_old/DB2SQL_DB_Tests.robot @@ -1,9 +1,8 @@ *** Settings *** Suite Setup Connect To Database ibm_db_dbi ${DBName} ${DBUser} ${DBPass} ${DBHost} ${DBPort} Suite Teardown Disconnect From Database +Resource DB2SQL_DB_Conf.txt Library DatabaseLibrary -Library Collections -Force Tags optional *** Test Cases *** Create person table @@ -12,8 +11,8 @@ Create person table Should Be Equal As Strings ${output} None Execute SQL Script - Insert Data person table - Comment ${output} = Execute SQL Script ${CURDIR}/my_db_test_insertData.sql - ${output} = Execute SQL Script ${CURDIR}/my_db_test_insertData.sql + Comment ${output} = Execute SQL Script ./my_db_test_insertData.sql + ${output} = Execute SQL Script ../test/my_db_test_insertData.sql Log ${output} Should Be Equal As Strings ${output} None @@ -54,20 +53,11 @@ Verify person Description @{queryResults} = Description SELECT * FROM person fetch first 1 rows only; Log Many @{queryResults} ${output} = Set Variable ${queryResults[0]} - Should Be Equal As Strings ${output[0]} ID - ${expected}= Evaluate ['DEC', 'NUMERIC', 'DECIMAL', 'NUM'] - Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True - Should Be Equal As Strings ${output[2:]} [12, 12, 10, 0, True] + Should Be Equal As Strings ${output} ['ID', DBAPITypeObject(['NUM', 'DECIMAL', 'DEC', 'NUMERIC']), 12, 12, 10, 0, True] ${output} = Set Variable ${queryResults[1]} - Should Be Equal As Strings ${output[0]} FIRST_NAME - ${expected}= Evaluate ['VARCHAR', 'CHARACTER VARYING', 'STRING', 'CHARACTER', 'CHAR', 'CHAR VARYING'] - Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True - Should Be Equal As Strings ${output[2:]} [30, 30, 30, 0, True] + Should Be Equal As Strings ${output} ['FIRST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] ${output} = Set Variable ${queryResults[2]} - Should Be Equal As Strings ${output[0]} LAST_NAME - ${expected}= Evaluate ['CHAR', 'CHAR VARYING', 'VARCHAR', 'CHARACTER VARYING', 'CHARACTER', 'STRING'] - Lists Should Be Equal ${output[1].col_types} ${expected} ignore_order=True - Should Be Equal As Strings ${output[2:]} [30, 30, 30, 0, True] + Should Be Equal As Strings ${output} ['LAST_NAME', DBAPITypeObject(['CHARACTER VARYING', 'CHAR VARYING', 'VARCHAR', 'STRING', 'CHARACTER', 'CHAR']), 30, 30, 30, 0, True] ${NumColumns} = Get Length ${queryResults} Should Be Equal As Integers ${NumColumns} 3 @@ -85,8 +75,8 @@ Verify Query - Get results as a list of dictionaries [Tags] db smoke ${output} = Query SELECT * FROM person; \ True Log ${output} - Should Be Equal As Strings ${output[0]['FIRST_NAME']} Franz Allan - Should Be Equal As Strings ${output[1]['FIRST_NAME']} Jerry + Should Be Equal As Strings &{output[0]}[first_name] Franz Allan + Should Be Equal As Strings &{output[1]}[first_name] Jerry Insert Data Into Table foobar ${output} = Execute SQL String INSERT INTO foobar VALUES(1,'Jerry'); @@ -106,7 +96,4 @@ Verify Query - Row Count foobar table 0 row Drop person and foobar table Execute SQL String DROP TABLE person; - Execute SQL String DROP TABLE foobar; - -Disconnect from all databases - Disconnect From All Databases + Execute SQL String DROP TABLE foobar; \ No newline at end of file diff --git a/test/tests/_old/MySQL_DB_Tests.robot b/test/tests/_old/MySQL_DB_Tests.robot index ac0114c4..59604851 100644 --- a/test/tests/_old/MySQL_DB_Tests.robot +++ b/test/tests/_old/MySQL_DB_Tests.robot @@ -177,6 +177,3 @@ Drop person and foobar tables ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; Log ${output} Should Be Equal As Strings ${output} None - -Disconnect from all databases - Disconnect From All Databases diff --git a/test/tests/_old/PostgreSQL_DB_Tests.robot b/test/tests/_old/PostgreSQL_DB_Tests.robot index d0998202..5cbc3f11 100644 --- a/test/tests/_old/PostgreSQL_DB_Tests.robot +++ b/test/tests/_old/PostgreSQL_DB_Tests.robot @@ -145,6 +145,3 @@ Drop person and foobar tables ${output} = Execute SQL String DROP TABLE IF EXISTS person,foobar; Log ${output} Should Be Equal As Strings ${output} None - -Disconnect from all databases - Disconnect From All Databases diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot index 1beabbfa..2b30ae16 100644 --- a/test/tests/common_tests/aliased_connection.robot +++ b/test/tests/common_tests/aliased_connection.robot @@ -1,13 +1,7 @@ *** Settings *** Resource ../../resources/common.resource -Suite Setup Run Keywords -... Connect To DB -... AND -... Create Person Table -Suite Teardown Run Keywords -... Connect To DB -... AND -... Drop Tables Person And Foobar +Suite Setup Aliased Suite Setup +Suite Teardown Aliased Suite Teardown Test Teardown Disconnect From All Databases @@ -116,14 +110,3 @@ Verify Delete All Rows From Table Connect To DB Aliased alias=aliased_conn Delete All Rows From Table person alias=aliased_conn Row Count Is 0 SELECT * FROM person alias=aliased_conn - - -*** Keywords *** -Aliases Suite Setup - Connect To DB Aliased - Create Person Table - -Aliases Suite Teardown - Connect To DB Aliased - Drop Tables Person And Foobar - Disconnect From All Databases From c4f2a72668ee3840d6e6b08a9413e6ae084794be Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Fri, 10 Nov 2023 11:08:34 +0100 Subject: [PATCH 06/15] Move connection to ConnectionStore class --- src/DatabaseLibrary/assertion.py | 2 +- src/DatabaseLibrary/connection_manager.py | 102 +++++++++++++--------- src/DatabaseLibrary/query.py | 14 +-- 3 files changed, 68 insertions(+), 50 deletions(-) diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 104a85bb..bb19b632 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -287,7 +287,7 @@ def table_must_exist( | Table Must Exist | first_name | msg=my error message | """ logger.info(f"Executing : Table Must Exist | {tableName}") - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) if db_connection.module_name in ["cx_Oracle", "oracledb"]: query = ( "SELECT * FROM all_objects WHERE object_type IN ('TABLE','VIEW') AND " diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 964b2141..09a42793 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -30,17 +30,12 @@ class Connection: module_name: str -class ConnectionManager: - """ - Connection Manager handles the connection & disconnection to the database. - """ - +class ConnectionStore: def __init__(self): - self.omit_trailing_semicolon: bool = False self._connections: Dict[str, Connection] = {} self.default_alias: str = "default" - def _register_connection(self, client: Any, module_name: str, alias: str): + def register_connection(self, client: Any, module_name: str, alias: str): if alias in self._connections: if alias == self.default_alias: logger.warn("Overwriting not closed connection.") @@ -48,6 +43,53 @@ def _register_connection(self, client: Any, module_name: str, alias: str): logger.warn(f"Overwriting not closed connection for alias = '{alias}'") self._connections[alias] = Connection(client, module_name) + def get_connection(self, alias: Optional[str]): + """ + Return connection with given alias. + + If alias is not provided, it will return default connection. + If there is no default connection, it will return last opened connection. + """ + if not self._connections: + raise ValueError(f"No database connection is open.") + if not alias: + if self.default_alias in self._connections: + return self._connections[self.default_alias] + return list(self._connections.values())[-1] + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + return self._connections[alias] + + def pop_connection(self, alias: Optional[str]): + if not self._connections: + return None + if not alias: + alias = self.default_alias + if alias not in self._connections: + alias = list(self._connections.keys())[-1] + return self._connections.pop(alias, None) + + def clear(self): + self._connections = {} + + def switch(self, alias: str): + if alias not in self._connections: + raise ValueError(f"Alias '{alias}' not found in existing connections.") + self.default_alias = alias + + def __iter__(self): + return iter(self._connections.values()) + + +class ConnectionManager: + """ + Connection Manager handles the connection & disconnection to the database. + """ + + def __init__(self): + self.omit_trailing_semicolon: bool = False + self.connection_store: ConnectionStore = ConnectionStore() + def connect_to_database( self, dbapiModuleName: Optional[str] = None, @@ -279,7 +321,7 @@ def connect_to_database( host=dbHost, port=dbPort, ) - self._register_connection(db_connection, db_api_module_name, alias) + self.connection_store.register_connection(db_connection, db_api_module_name, alias) def connect_to_database_using_custom_params( self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: str = "default" @@ -317,7 +359,7 @@ def connect_to_database_using_custom_params( ) db_connection = eval(db_connect_string) - self._register_connection(db_connection, db_api_module_name, alias) + self.connection_store.register_connection(db_connection, db_api_module_name, alias) def connect_to_database_using_custom_connection_string( self, dbapiModuleName: Optional[str] = None, db_connect_string: str = "", alias: str = "default" @@ -341,7 +383,7 @@ def connect_to_database_using_custom_connection_string( f"'{db_connect_string}')" ) db_connection = db_api_2.connect(db_connect_string) - self._register_connection(db_connection, db_api_module_name, alias) + self.connection_store.register_connection(db_connection, db_api_module_name, alias) def disconnect_from_database(self, error_if_no_connection: bool = False, alias: Optional[str] = None): """ @@ -356,19 +398,14 @@ def disconnect_from_database(self, error_if_no_connection: bool = False, alias: | Disconnect From Database | alias=my_alias | # disconnects from current connection to the database | """ logger.info("Executing : Disconnect From Database") - if not alias: - if not self._connections or self.default_alias in self._connections: - alias = self.default_alias - else: - alias = list(self._connections.keys())[-1] - try: - db_connection = self._connections.pop(alias) - db_connection.client.close() - except KeyError: # Non-existing alias + db_connection = self.connection_store.pop_connection(alias) + if db_connection is None: log_msg = "No open database connection to close" if error_if_no_connection: raise ConnectionError(log_msg) from None logger.info(log_msg) + else: + db_connection.client.close() def disconnect_from_all_databases(self): """ @@ -378,9 +415,9 @@ def disconnect_from_all_databases(self): | Disconnect From All Databases | # disconnects from all connections to the database | """ logger.info("Executing : Disconnect From All Databases") - for db_connection in self._connections.values(): + for db_connection in self.connection_store: db_connection.client.close() - self._connections = {} + self.connection_store.clear() def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = None): """ @@ -400,7 +437,7 @@ def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = None): | Set Auto Commit | False """ logger.info("Executing : Set Auto Commit") - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) db_connection.client.autocommit = autoCommit def switch_database(self, alias: str): @@ -411,23 +448,4 @@ def switch_database(self, alias: str): | Switch Database | my_alias | | Switch Database | alias=my_alias | """ - if alias not in self._connections: - raise ValueError(f"Alias '{alias}' not found in existing connections.") - self.default_alias = alias - - def _get_connection_with_alias(self, alias: Optional[str]) -> Connection: - """ - Return connection with given alias. - - If alias is not provided, it will return default connection. - If there is no default connection, it will return last opened connection. - """ - if not self._connections: - raise ValueError(f"No database connection is open.") - if not alias: - if self.default_alias in self._connections: - return self._connections[self.default_alias] - return list(self._connections.values())[-1] - if alias not in self._connections: - raise ValueError(f"Alias '{alias}' not found in existing connections.") - return self._connections[alias] + self.connection_store.switch(alias) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index e5b6cb45..2da52fe8 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -64,7 +64,7 @@ def query( Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | @{queryResults} | Query | SELECT * FROM person | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) cur = None try: cur = db_connection.client.cursor() @@ -110,7 +110,7 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: | ${rowCount} | Row Count | SELECT * FROM person | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) cur = None try: cur = db_connection.client.cursor() @@ -149,7 +149,7 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Description | SELECT * FROM person | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) cur = None try: cur = db_connection.client.cursor() @@ -187,7 +187,7 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Delete All Rows From Table | person | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) cur = None query = f"DELETE FROM {tableName}" try: @@ -265,7 +265,7 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) with open(sqlScriptFileName, encoding="UTF-8") as sql_file: cur = None try: @@ -351,7 +351,7 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Opti Using optional `sansTran` to run command without an explicit transaction commit or rollback: | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) cur = None try: cur = db_connection.client.cursor() @@ -407,7 +407,7 @@ def call_stored_procedure( Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{Param values} @{Result sets} = | Call Stored Procedure | DBName.SchemaName.StoredProcName | ${Params} | True | """ - db_connection = self._get_connection_with_alias(alias) + db_connection = self.connection_store.get_connection(alias) if spParams is None: spParams = [] cur = None From e353506469e6aac5bace0e8067b78b404ae8266f Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 18:31:38 +0100 Subject: [PATCH 07/15] Improve keywords docs in assertions --- src/DatabaseLibrary/assertion.py | 182 ++++++++++--------------------- 1 file changed, 58 insertions(+), 124 deletions(-) diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index bb19b632..64a8723a 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -26,30 +26,21 @@ def check_if_exists_in_database( ): """ Check if any row would be returned by given the input ``selectStatement``. If there are no results, then this will - throw an AssertionError. Set optional input ``sansTran`` to True to run command without an explicit transaction - commit or rollback. The default error message can be overridden with the ``msg`` argument. + throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction + commit or rollback. - When you have the following assertions in your robot - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | - - Then you will get the following: - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # PASS | - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # FAIL | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: - | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | - - Using optional ``msg`` to override the default error message: + Examples: + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | msg=my error message | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | + | Check If Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True | """ logger.info(f"Executing : Check If Exists In Database | {selectStatement}") if not self.query(selectStatement, sansTran, alias=alias): @@ -64,30 +55,20 @@ def check_if_not_exists_in_database( This is the negation of `check_if_exists_in_database`. Check if no rows would be returned by given the input ``selectStatement``. If there are any results, then this - will throw an AssertionError. Set optional input ``sansTran`` to True to run command without an explicit - transaction commit or rollback. The default error message can be overridden with the ``msg`` argument. - - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + will throw an AssertionError. - When you have the following assertions in your robot - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - Then you will get the following: - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | # PASS | - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: - | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | True | - - Using optional ``msg`` to override the default error message: + Examples: + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'Franz Allan' | alias=my_alias | + | Check If Not Exists In Database | SELECT id FROM person WHERE first_name = 'John' | sansTran=True | """ logger.info(f"Executing : Check If Not Exists In Database | {selectStatement}") query_results = self.query(selectStatement, sansTran, alias=alias) @@ -101,30 +82,21 @@ def row_count_is_0( ): """ Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an - AssertionError. Set optional input ``sansTran`` to True to run command without an explicit transaction commit or - rollback. The default error message can be overridden with the ``msg`` argument. - - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | + AssertionError. - When you have the following assertions in your robot - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | alias=my_alias | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or + rollback. - Then you will get the following: - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | # FAIL | - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | # PASS | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: - | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | True | - - Using optional `msg` to override the default error message: + Examples: + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | | Row Count is 0 | SELECT id FROM person WHERE first_name = 'Franz Allan' | msg=my error message | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | alias=my_alias | + | Row Count is 0 | SELECT id FROM person WHERE first_name = 'John' | sansTran=True | """ logger.info(f"Executing : Row Count Is 0 | {selectStatement}") num_rows = self.row_count(selectStatement, sansTran, alias=alias) @@ -140,32 +112,21 @@ def row_count_is_equal_to_x( alias: Optional[str] = None, ): """ - Check if the number of rows returned from `selectStatement` is equal to the value submitted. If not, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. The default error message can be overridden with the `msg` argument. + Check if the number of rows returned from ``selectStatement`` is equal to the value submitted. If not, then this + will throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - When you have the following assertions in your robot - | Row Count Is Equal To X | SELECT id FROM person | 1 | - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | - - Then you will get the following: - | Row Count Is Equal To X | SELECT id FROM person | 1 | # FAIL | - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | # PASS | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | True | - - Using optional `msg` to override the default error message: - | Row Count Is Equal To X | SELECT id FROM person | 1 | msg=my error message | + Examples: + | Row Count Is Equal To X | SELECT id FROM person | 1 | + | Row Count Is Equal To X | SELECT id FROM person | 3 | msg=my error message | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | + | Row Count Is Equal To X | SELECT id FROM person WHERE first_name = 'John' | 0 | sansTran=True | """ logger.info(f"Executing : Row Count Is Equal To X | {selectStatement} | {numRows}") num_rows = self.row_count(selectStatement, sansTran, alias=alias) @@ -183,32 +144,21 @@ def row_count_is_greater_than_x( alias: Optional[str] = None, ): """ - Check if the number of rows returned from `selectStatement` is greater than the value submitted. If not, then - this will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. The default error message can be overridden with the `msg` argument. + Check if the number of rows returned from ``selectStatement`` is greater than the value submitted. If not, then + this will throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - When you have the following assertions in your robot - | Row Count Is Greater Than X | SELECT id FROM person | 1 | - | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | - | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | - - Then you will get the following: - | Row Count Is Greater Than X | SELECT id FROM person | 1 | # PASS | - | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | # FAIL | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Greater Than X | SELECT id FROM person | 1 | True | - - Using optional `msg` to override the default error message: + Examples: + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | msg=my error message | + | Row Count Is Greater Than X | SELECT id FROM person WHERE first_name = 'John' | 0 | alias=my_alias | + | Row Count Is Greater Than X | SELECT id FROM person | 1 | sansTran=True | """ logger.info(f"Executing : Row Count Is Greater Than X | {selectStatement} | {numRows}") num_rows = self.row_count(selectStatement, sansTran, alias=alias) @@ -226,32 +176,22 @@ def row_count_is_less_than_x( alias: Optional[str] = None, ): """ - Check if the number of rows returned from `selectStatement` is less than the value submitted. If not, then this - will throw an AssertionError. Set optional input `sansTran` to True to run command without an explicit - transaction commit or rollback. + Check if the number of rows returned from ``selectStatement`` is less than the value submitted. If not, then this + will throw an AssertionError. - For example, given we have a table `person` with the following data: - | id | first_name | last_name | - | 1 | Franz Allan | See | - | 2 | Jerry | Schneider | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction commit or rollback. - When you have the following assertions in your robot - | Row Count Is Less Than X | SELECT id FROM person | 3 | - | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | - | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | alias=my_alias | - - Then you will get the following: - | Row Count Is Less Than X | SELECT id FROM person | 3 | # PASS | - | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | # FAIL | + Using optional ``msg`` to override the default error message: Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Row Count Is Less Than X | SELECT id FROM person | 3 | True | + Examples: + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 2 | msg=my error message | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 3 | alias=my_alias | + | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 4 | sansTran=True | - Using optional `msg` to override the default error message: - | Row Count Is Less Than X | SELECT id FROM person WHERE first_name = 'John' | 1 | msg=my error message | """ logger.info(f"Executing : Row Count Is Less Than X | {selectStatement} | {numRows}") num_rows = self.row_count(selectStatement, sansTran, alias=alias) @@ -264,27 +204,21 @@ def table_must_exist( self, tableName: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None ): """ - Check if the table given exists in the database. Set optional input `sansTran` to True to run command without an - explicit transaction commit or rollback. The default error message can be overridden with the `msg` argument. - - For example, given we have a table `person` in a database + Check if the table given exists in the database. - When you do the following: - | Table Must Exist | person | + Set optional input ``sansTran`` to True to run command without an + explicit transaction commit or rollback. - Then you will get the following: - | Table Must Exist | person | # PASS | - | Table Must Exist | first_name | # FAIL | - | Table Must Exist | first_name | alias=my_alias | + The default error message can be overridden with the ``msg`` argument. Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Table Must Exist | person | True | - - Using optional `msg` to override the default error message: - | Table Must Exist | first_name | msg=my error message | + Examples: + | Table Must Exist | person | + | Table Must Exist | person | msg=my error message | + | Table Must Exist | person | alias=my_alias | + | Table Must Exist | person | sansTran=True | """ logger.info(f"Executing : Table Must Exist | {tableName}") db_connection = self.connection_store.get_connection(alias) From 82d27bfa51fa4729336f5c652d6e8117506223ec Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 18:39:22 +0100 Subject: [PATCH 08/15] Improve docs for new keyword "disconnect from all databases" --- src/DatabaseLibrary/connection_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 09a42793..9b66aee1 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -409,10 +409,11 @@ def disconnect_from_database(self, error_if_no_connection: bool = False, alias: def disconnect_from_all_databases(self): """ - Disconnects from all the databases. + Disconnects from all the databases - + useful when testing with multiple database connections (aliases). For example: - | Disconnect From All Databases | # disconnects from all connections to the database | + | Disconnect From All Databases | # Closes connections to all databases | """ logger.info("Executing : Disconnect From All Databases") for db_connection in self.connection_store: From 3cfbd5e03182c0f81a476df2cc21010643268f49 Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 19:24:18 +0100 Subject: [PATCH 09/15] Improve docs for new keyword "switch database" --- src/DatabaseLibrary/connection_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 9b66aee1..9ae23d60 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -443,9 +443,9 @@ def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = None): def switch_database(self, alias: str): """ - Switch default database. + Switch the default database connection to ``alias``. - Example: + Examples: | Switch Database | my_alias | | Switch Database | alias=my_alias | """ From 73797422119cd97103c9e095615caeddf363fbde Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 19:53:13 +0100 Subject: [PATCH 10/15] Improve naming and location of keywords for aliased connection tests --- test/resources/common.resource | 10 --- .../common_tests/aliased_connection.robot | 71 +++++++++++-------- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/test/resources/common.resource b/test/resources/common.resource index 9ae0764f..1acf6e31 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -84,16 +84,6 @@ Connect To DB Aliased Fail Unexpected mode - ${DB_MODULE_MODE} END -Aliased Suite Setup - Connect To DB - Create Person Table - Disconnect From Database - -Aliased Suite Teardown - Connect To DB - Drop Tables Person And Foobar - Disconnect From Database - Build Connection String [Documentation] Returns the connection string variable depending on the DB module ... currently under test. diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot index 2b30ae16..2cb007c4 100644 --- a/test/tests/common_tests/aliased_connection.robot +++ b/test/tests/common_tests/aliased_connection.robot @@ -1,107 +1,108 @@ *** Settings *** Resource ../../resources/common.resource -Suite Setup Aliased Suite Setup -Suite Teardown Aliased Suite Teardown -Test Teardown Disconnect From All Databases + +Suite Setup Connect, Create Some Data And Disconnect +Suite Teardown Connect, Clean Up Data And Disconnect +Test Teardown Disconnect From All Databases *** Test Cases *** Connections Can Be Aliased - Connect To DB Aliased # default alias - Connect To DB Aliased alias=second + Connect To DB # default alias + Connect To DB alias=second Default Alias Can Be Empty - Connect To DB Aliased # default alias + Connect To DB # default alias Query SELECT * FROM person - Connect To DB Aliased alias=second + Connect To DB alias=second Query SELECT * FROM person Query SELECT * FROM person alias=second Switch From Default And Disconnect - Connect To DB Aliased # default alias - Connect To DB Aliased alias=second + Connect To DB # default alias + Connect To DB alias=second Switch Database second - Query SELECT * FROM person # query with 'second' connection + Query SELECT * FROM person # query with 'second' connection Disconnect From Database alias=second - Query SELECT * FROM person # query with 'default' connection + Query SELECT * FROM person # query with 'default' connection Disconnect Not Existing Alias - Connect To DB Aliased # default alias - Disconnect From Database alias=idontexist # silent warning + Connect To DB # default alias + Disconnect From Database alias=idontexist # silent warning Run Keyword And Expect Error ConnectionError: No open database connection to close ... Disconnect From Database alias=idontexist error_if_no_connection=${True} # default alias exist and can be closed - Disconnect From Database error_if_no_connection=${True} + Disconnect From Database error_if_no_connection=${True} Switch Not Existing Alias Run Keyword And Expect Error ValueError: Alias 'second' not found in existing connections. ... Switch Database second Execute SQL Script - Insert Data In Person table - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn ${output} Insert Data In Person Table Using SQL Script Aliased alias=aliased_conn Should Be Equal As Strings ${output} None Check If Exists In DB - Franz Allan - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Check If Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Franz Allan' alias=aliased_conn Check If Not Exists In DB - Joe - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Check If Not Exists In Database SELECT id FROM person WHERE FIRST_NAME= 'Joe' alias=aliased_conn Table Must Exist - person - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Table Must Exist person alias=aliased_conn Verify Row Count is 0 - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Row Count is 0 SELECT * FROM person WHERE FIRST_NAME= 'NotHere' alias=aliased_conn Verify Row Count is Equal to X - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Row Count is Equal to X SELECT id FROM person 2 alias=aliased_conn Verify Row Count is Less Than X - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Row Count is Less Than X SELECT id FROM person 3 alias=aliased_conn Verify Row Count is Greater Than X - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Row Count is Greater Than X SELECT * FROM person 1 alias=aliased_conn Retrieve Row Count - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn ${output} Row Count SELECT id FROM person alias=aliased_conn Log ${output} Should Be Equal As Strings ${output} 2 Retrieve records from person table - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn ${output} Execute SQL String SELECT * FROM person Log ${output} Should Be Equal As Strings ${output} None Use Last Connected If Not Alias Provided - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn ${output} Query SELECT COUNT(*) FROM person Log ${output} Should Be Equal As Integers ${output}[0][0] 2 Verify Query - Get results as a list of dictionaries - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn ${output} Query SELECT * FROM person returnAsDict=True alias=aliased_conn Log ${output} # some databases lower field names and you can't do anything about it TRY - ${value 1}= Get From Dictionary ${output}[0] FIRST_NAME + ${value 1} Get From Dictionary ${output}[0] FIRST_NAME EXCEPT Dictionary does not contain key 'FIRST_NAME'. - ${value 1}= Get From Dictionary ${output}[0] first_name + ${value 1} Get From Dictionary ${output}[0] first_name END TRY - ${value 2}= Get From Dictionary ${output}[1] FIRST_NAME + ${value 2} Get From Dictionary ${output}[1] FIRST_NAME EXCEPT Dictionary does not contain key 'FIRST_NAME'. - ${value 2}= Get From Dictionary ${output}[1] first_name + ${value 2} Get From Dictionary ${output}[1] first_name END Should Be Equal As Strings ${value 1} Franz Allan Should Be Equal As Strings ${value 2} Jerry @@ -110,3 +111,13 @@ Verify Delete All Rows From Table Connect To DB Aliased alias=aliased_conn Delete All Rows From Table person alias=aliased_conn Row Count Is 0 SELECT * FROM person alias=aliased_conn +*** Keywords *** +Connect, Create Some Data And Disconnect + Connect To DB + Create Person Table + Disconnect From Database + +Connect, Clean Up Data And Disconnect + Connect To DB + Drop Tables Person And Foobar + Disconnect From Database From 89c95826adf1a69841bcf9ace2dd426718a39398 Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 19:53:49 +0100 Subject: [PATCH 11/15] Merge two global "connect to db" keywords in tests --- test/resources/common.resource | 48 +++---------------- .../common_tests/aliased_connection.robot | 4 +- 2 files changed, 9 insertions(+), 43 deletions(-) diff --git a/test/resources/common.resource b/test/resources/common.resource index 1acf6e31..9e2a43c7 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -22,40 +22,6 @@ ${DB_DRIVER} ODBC Driver 18 for SQL Server *** Keywords *** Connect To DB - [Documentation] Connects to the database based on the current DB module under test - ... and connection params set in global variables - IF "${DB_MODULE_MODE}" == "custom" - IF "${DB_MODULE}" == "sqlite3" - Remove File ${DBName}.db - Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None - ELSE - ${Connection String}= Build Connection String - Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} - END - ELSE IF "${DB_MODULE_MODE}" == "standard" - IF "${DB_MODULE}" == "pyodbc" - Connect To Database - ... ${DB_MODULE} - ... ${DB_NAME} - ... ${DB_USER} - ... ${DB_PASS} - ... ${DB_HOST} - ... ${DB_PORT} - ... dbDriver=${DB_DRIVER} - ELSE - Connect To Database - ... ${DB_MODULE} - ... ${DB_NAME} - ... ${DB_USER} - ... ${DB_PASS} - ... ${DB_HOST} - ... ${DB_PORT} - END - ELSE - Fail Unexpected mode - ${DB_MODULE_MODE} - END - -Connect To DB Aliased [Documentation] Connects to the database based on the current DB module under test ... and connection params set in global variables with alias [Arguments] ${alias}=${None} @@ -63,15 +29,13 @@ Connect To DB Aliased IF $alias is not None Set To Dictionary ${DB_KWARGS} alias=${alias} IF "${DB_MODULE_MODE}" == "custom" IF "${DB_MODULE}" == "sqlite3" - IF $alias is None - # Remove File ${DBName}.db TODO Figure out when clear db for aliased tests - Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None - ELSE - Connect To Database Using Custom Params - ... sqlite3 - ... database="./${DBName}.db", isolation_level=None - ... alias=${alias} + ${DB_Filename}= Set Variable ${DBName} + IF $alias is not None + ${DB_Filename}= Set Variable ${DB_Filename}_${alias} END + Remove File ${DB_Filename}.db + Connect To Database Using Custom Params sqlite3 database="./${DB_Filename}.db", isolation_level=None + ... &{DB_KWARGS} ELSE ${Connection String}= Build Connection String Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} &{DB_KWARGS} diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot index 2cb007c4..ab2c84b0 100644 --- a/test/tests/common_tests/aliased_connection.robot +++ b/test/tests/common_tests/aliased_connection.robot @@ -108,9 +108,11 @@ Verify Query - Get results as a list of dictionaries Should Be Equal As Strings ${value 2} Jerry Verify Delete All Rows From Table - Connect To DB Aliased alias=aliased_conn + Connect To DB alias=aliased_conn Delete All Rows From Table person alias=aliased_conn Row Count Is 0 SELECT * FROM person alias=aliased_conn + + *** Keywords *** Connect, Create Some Data And Disconnect Connect To DB From d3adab0b1f27b96f193b55eca3b2150e4e53f358 Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 20:41:13 +0100 Subject: [PATCH 12/15] Aliases tests don't work for SQLite as each connection is always a new file --- test/resources/common.resource | 20 ++++++++----------- .../common_tests/aliased_connection.robot | 15 ++++++++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/test/resources/common.resource b/test/resources/common.resource index 9e2a43c7..34831bc7 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -29,12 +29,8 @@ Connect To DB IF $alias is not None Set To Dictionary ${DB_KWARGS} alias=${alias} IF "${DB_MODULE_MODE}" == "custom" IF "${DB_MODULE}" == "sqlite3" - ${DB_Filename}= Set Variable ${DBName} - IF $alias is not None - ${DB_Filename}= Set Variable ${DB_Filename}_${alias} - END - Remove File ${DB_Filename}.db - Connect To Database Using Custom Params sqlite3 database="./${DB_Filename}.db", isolation_level=None + Remove File ${DBName}.db + Connect To Database Using Custom Params sqlite3 database="./${DBName}.db", isolation_level=None ... &{DB_KWARGS} ELSE ${Connection String}= Build Connection String @@ -74,12 +70,12 @@ Create Person Table And Insert Data Insert Data In Person Table Using SQL Script Insert Data In Person Table Using SQL Script - ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql - RETURN ${output} - -Insert Data In Person Table Using SQL Script Aliased - [Arguments] ${alias} - ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql alias=${alias} + [Arguments] ${alias}=${None} + IF $alias is None + ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql + ELSE + ${output}= Execute SQL Script ${CURDIR}/insert_data_in_person_table.sql alias=${alias} + END RETURN ${output} Create Foobar Table diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot index ab2c84b0..e61b3904 100644 --- a/test/tests/common_tests/aliased_connection.robot +++ b/test/tests/common_tests/aliased_connection.robot @@ -1,9 +1,10 @@ *** Settings *** Resource ../../resources/common.resource +Suite Setup Skip If "${DB_MODULE}" == "sqlite3" +... Aliases tests don't work for SQLite as each connection is always a new file -Suite Setup Connect, Create Some Data And Disconnect -Suite Teardown Connect, Clean Up Data And Disconnect -Test Teardown Disconnect From All Databases +Test Setup Connect, Create Some Data And Disconnect +Test Teardown Connect, Clean Up Data And Disconnect *** Test Cases *** @@ -39,8 +40,9 @@ Switch Not Existing Alias ... Switch Database second Execute SQL Script - Insert Data In Person table + [Setup] Connect, Create Some Data And Disconnect Run SQL script=${False} Connect To DB alias=aliased_conn - ${output} Insert Data In Person Table Using SQL Script Aliased alias=aliased_conn + ${output} Insert Data In Person Table Using SQL Script alias=aliased_conn Should Be Equal As Strings ${output} None Check If Exists In DB - Franz Allan @@ -115,11 +117,16 @@ Verify Delete All Rows From Table *** Keywords *** Connect, Create Some Data And Disconnect + [Arguments] ${Run SQL script}=${True} Connect To DB Create Person Table + IF $Run_SQL_script + Insert Data In Person Table Using SQL Script + END Disconnect From Database Connect, Clean Up Data And Disconnect + Disconnect From All Databases Connect To DB Drop Tables Person And Foobar Disconnect From Database From abca11055d37c7b41e9771deeccbeb58dfb0a975 Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 21:07:23 +0100 Subject: [PATCH 13/15] Test for real multiple connections --- .../multiple_connections.robot | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 test/tests/custom_db_tests/multiple_connections.robot diff --git a/test/tests/custom_db_tests/multiple_connections.robot b/test/tests/custom_db_tests/multiple_connections.robot new file mode 100644 index 00000000..540aee71 --- /dev/null +++ b/test/tests/custom_db_tests/multiple_connections.robot @@ -0,0 +1,62 @@ +*** Settings *** +Documentation Connections to two different databases can be handled separately. +... These tests require two databases running in parallel. + +Resource ../../resources/common.resource + +Suite Setup Connect To All Databases +Suite Teardown Disconnect From All Databases +Test Setup Create Tables +Test Teardown Drop Tables + + +*** Variables *** +${Table_1} table_1 +${Table_2} table_2 + +${Alias_1} first +${Alias_2} second + + +*** Test Cases *** +First Table Was Created In First Database Only + Table Must Exist ${Table_1} alias=${Alias_1} + Run Keyword And Expect Error Table '${Table_2}' does not exist in the db + ... Table Must Exist ${Table_2} alias=${Alias_1} + +Second Table Was Created In Second Database Only + Table Must Exist ${Table_2} alias=${Alias_2} + Run Keyword And Expect Error Table '${Table_1}' does not exist in the db + ... Table Must Exist ${Table_1} alias=${Alias_2} + +Switching Default Alias + Switch Database ${Alias_1} + Table Must Exist ${Table_1} + Run Keyword And Expect Error Table '${Table_2}' does not exist in the db + ... Table Must Exist ${Table_2} + Switch Database ${Alias_2} + Table Must Exist ${Table_2} + Run Keyword And Expect Error Table '${Table_1}' does not exist in the db + ... Table Must Exist ${Table_1} + + +*** Keywords *** +Connect To All Databases + Connect To Database psycopg2 db db_user pass 127.0.0.1 5432 + ... alias=${Alias_1} + Connect To Database pymysql db db_user pass 127.0.0.1 3306 + ... alias=${Alias_2} + +Create Tables + ${sql_1}= Catenate + ... CREATE TABLE ${Table_1} + ... (id integer not null unique, FIRST_NAME varchar(20), LAST_NAME varchar(20)) + ${sql_2}= Catenate + ... CREATE TABLE ${Table_2} + ... (id integer not null unique, FIRST_NAME varchar(20), LAST_NAME varchar(20)) + Execute Sql String ${sql_1} alias=${Alias_1} + Execute Sql String ${sql_2} alias=${Alias_2} + +Drop Tables + Execute Sql String DROP TABLE ${Table_1} alias=${Alias_1} + Execute Sql String DROP TABLE ${Table_2} alias=${Alias_2} From 5f5092054c286fafdeedfe8630ea3039739ab79a Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 21:22:50 +0100 Subject: [PATCH 14/15] Add example with aliases into readme --- README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a714ba3..6c37d4dd 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ Here you can find the [keyword docs](http://marketsquare.github.io/Robotframewor ``` pip install robotframework-databaselibrary ``` -# Usage example +# Usage examples +## Basic usage ```RobotFramework *** Settings *** Library DatabaseLibrary @@ -42,6 +43,32 @@ Person Table Contains No Joe ... WHERE FIRST_NAME= 'Joe' Check If Not Exists In Database ${sql} ``` +## Handling multiple database connections +```RobotFramework +*** Settings *** +Library DatabaseLibrary +Test Setup Connect To All Databases +Test Teardown Disconnect From All Databases + +*** Keywords *** +Connect To All Databases + Connect To Database psycopg2 db db_user pass 127.0.0.1 5432 + ... alias=postgres + Connect To Database pymysql db db_user pass 127.0.0.1 3306 + ... alias=mysql + +*** Test Cases *** +Using Aliases + ${names}= Query select LAST_NAME from person alias=postgres + Execute Sql String drop table XYZ alias=mysql + +Switching Default Alias + Switch Database postgres + ${names}= Query select LAST_NAME from person + Switch Database mysql + Execute Sql String drop table XYZ +``` + See more examples in the folder `tests`. # Database modules compatibility The library is basically compatible with any [Python Database API Specification 2.0](https://peps.python.org/pep-0249/) module. From 3721d08124b45679c8ad257629abb640a90209a5 Mon Sep 17 00:00:00 2001 From: amochin Date: Sat, 18 Nov 2023 21:24:58 +0100 Subject: [PATCH 15/15] Bump version to 1.4 - slowly getting ready for release --- src/DatabaseLibrary/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DatabaseLibrary/version.py b/src/DatabaseLibrary/version.py index 4cf03a8d..5d4d63cb 100644 --- a/src/DatabaseLibrary/version.py +++ b/src/DatabaseLibrary/version.py @@ -1 +1 @@ -VERSION = "1.3.1" +VERSION = "1.4"