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/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. 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 185c2ba2..64a8723a 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -21,245 +21,231 @@ 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 - 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 | + Check if any row would be returned by given the input ``selectStatement``. If there are no results, then this will + throw an AssertionError. - 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' | + Set optional input ``sansTran`` to _True_ to run command without an explicit transaction + commit or rollback. - 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. - 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 | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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): + 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`. - 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 | + Check if no rows would be returned by given the input ``selectStatement``. If there are any results, then this + 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' | + 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. - 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 | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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) + 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 - 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. - 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 - | 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' | - - 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. - 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 | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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) + 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 - 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 | - - 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. - 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 | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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) + 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 - 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 | - - 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. - 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 | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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) + 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 - 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 | + Using optional ``msg`` to override the default error message: - 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 | + 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) + 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. + Check if the table given exists in the database. - For example, given we have a table `person` in a database + Set optional input ``sansTran`` to True to run command without an + explicit transaction commit or rollback. - When you do the following: - | Table Must Exist | person | - - Then you will get the following: - | Table Must Exist | person | # PASS | - | Table Must Exist | first_name | # FAIL | + The default error message can be overridden with the ``msg`` argument. - Using optional `sansTran` to run command without an explicit transaction commit or rollback: - | Table Must Exist | person | True | + Use optional ``alias`` parameter to specify what connection should be used for the query if you have more + than one connection open. - 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}") - if self.db_api_module_name in ["cx_Oracle", "oracledb"]: + 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 " 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_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) > 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_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) > 0 - elif self.db_api_module_name in ["teradata"]: + table_exists = self.row_count(query, sansTran, alias=alias) > 0 + 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) > 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..9ae23d60 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 @@ -23,18 +24,71 @@ from robot.api import logger +@dataclass +class Connection: + client: Any + module_name: str + + +class ConnectionStore: + def __init__(self): + 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: + 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 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): - """ - Initializes _dbconnection to None. - """ - self._dbconnection = None - self.db_api_module_name = None - self.omit_trailing_semicolon = False + self.omit_trailing_semicolon: bool = False + self.connection_store: ConnectionStore = ConnectionStore() def connect_to_database( self, @@ -48,20 +102,24 @@ def connect_to_database( dbDriver: Optional[str] = None, dbConfigFile: Optional[str] = None, driverMode: Optional[str] = None, + 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`. + 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 - 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`. @@ -70,7 +128,7 @@ def connect_to_database( your database credentials. Example db.cfg file - | [default] + | [alias] | dbapiModuleName=pymysqlforexample | dbName=yourdbname | dbUsername=yourusername @@ -81,6 +139,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 +159,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 +179,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 +193,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 +210,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 +228,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 +237,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 +248,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 +274,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 +287,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 +302,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 +314,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.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 = "" + 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 @@ -281,7 +341,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 +358,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.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 = "" + 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 @@ -316,14 +377,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.connection_store.register_connection(db_connection, db_api_module_name, 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] = None): """ Disconnects from the database. @@ -333,18 +395,32 @@ 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: + 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) + raise ConnectionError(log_msg) from None logger.info(log_msg) else: - self._dbconnection.close() - self._dbconnection = None + db_connection.client.close() - def set_auto_commit(self, autoCommit: bool = True): + def disconnect_from_all_databases(self): + """ + Disconnects from all the databases - + useful when testing with multiple database connections (aliases). + + For example: + | Disconnect From All Databases | # Closes connections to all databases | + """ + logger.info("Executing : Disconnect From All Databases") + for db_connection in self.connection_store: + db_connection.client.close() + self.connection_store.clear() + + def set_auto_commit(self, autoCommit: bool = True, alias: Optional[str] = None): """ Turn the autocommit on the database connection ON or OFF. @@ -357,8 +433,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.connection_store.get_connection(alias) + db_connection.client.autocommit = autoCommit + + def switch_database(self, alias: str): + """ + Switch the default database connection to ``alias``. + + Examples: + | Switch Database | my_alias | + | Switch Database | alias=my_alias | + """ + self.connection_store.switch(alias) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index f4c47867..2da52fe8 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -24,11 +24,16 @@ 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. - 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 @@ -43,6 +48,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: @@ -55,13 +61,14 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool 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.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() - logger.info(f"Executing : Query | {selectStatement}") + cur = db_connection.client.cursor() + logger.info(f"Executing : Query | {selectStatement} ") self.__execute_sql(cur, selectStatement) all_rows = cur.fetchall() if returnAsDict: @@ -70,12 +77,12 @@ def query(self, selectStatement: str, sansTran: bool = False, returnAsDict: bool return all_rows finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.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. + 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 | @@ -84,6 +91,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: @@ -96,26 +104,30 @@ def row_count(self, selectStatement: str, sansTran: bool = False): 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.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.client.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_connection.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.client.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. + 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 | @@ -123,6 +135,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: @@ -130,12 +143,16 @@ def description(self, selectStatement: str, sansTran: bool = False): [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 | """ + db_connection = self.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.client.cursor() logger.info("Executing : Description | {selectStatement}") self.__execute_sql(cur, selectStatement) description = list(cur.description) @@ -145,9 +162,9 @@ def description(self, selectStatement: str, sansTran: bool = False): return description finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.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 +173,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 | @@ -163,35 +181,39 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False): 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 | """ + db_connection = self.connection_store.get_connection(alias) cur = None query = f"DELETE FROM {tableName}" try: - cur = self._dbconnection.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: - self._dbconnection.commit() + db_connection.client.commit() return result if not sansTran: - self._dbconnection.commit() + db_connection.client.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.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 `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 | + | 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 | @@ -237,14 +259,18 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False): 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 | """ + db_connection = self.connection_store.get_connection(alias) with open(sqlScriptFileName, encoding="UTF-8") as sql_file: cur = None try: statements_to_execute = [] - cur = self._dbconnection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Execute SQL Script | {sqlScriptFileName}") current_statement = "" inside_statements_group = False @@ -300,12 +326,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.client.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.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,25 +340,32 @@ 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} | + 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 | """ + db_connection = self.connection_store.get_connection(alias) cur = None try: - cur = self._dbconnection.cursor() + cur = db_connection.client.cursor() logger.info(f"Executing : Execute SQL String | {sqlString}") self.__execute_sql(cur, sqlString) if not sansTran: - self._dbconnection.commit() + db_connection.client.commit() finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.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 - @@ -368,23 +401,27 @@ def call_stored_procedure(self, spName: str, spParams: Optional[List[str]] = Non | # ${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 | """ + db_connection = self.connection_store.get_connection(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_connection.module_name == "pymssql": + cur = db_connection.client.cursor(as_dict=False) else: - cur = self._dbconnection.cursor() + cur = db_connection.client.cursor() param_values = [] result_sets = [] - if self.db_api_module_name == "pymysql": + if db_connection.module_name == "pymysql": cur.callproc(spName, spParams) # first proceed the result sets if available @@ -401,22 +438,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_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 = self._dbconnection.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 self.db_api_module_name in ["psycopg2", "psycopg3"]: - cur = self._dbconnection.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() @@ -433,7 +470,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_connection.module_name in ["psycopg3"]: result_sets_available = True while result_sets_available: result_sets.append(list(cur.fetchall())) @@ -444,10 +481,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_connection.module_name}' is not tested, " "results might be invalid!" ) - cur = self._dbconnection.cursor() + cur = db_connection.client.cursor() param_values = cur.callproc(spName, spParams) logger.info("Reading the procedure results..") result_sets_available = True @@ -463,12 +500,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.client.commit() return param_values, result_sets finally: if cur and not sansTran: - self._dbconnection.rollback() + db_connection.client.rollback() def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Optional[bool] = None): """ 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" diff --git a/test/resources/common.resource b/test/resources/common.resource index 6b78a1be..34831bc7 100644 --- a/test/resources/common.resource +++ b/test/resources/common.resource @@ -23,34 +23,23 @@ ${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 + ... 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" 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 - Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} + Connect To Database Using Custom Connection String ${DB_MODULE} ${Connection String} &{DB_KWARGS} 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 + ${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 @@ -81,7 +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 + [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/_old/DB2SQL_DB_Tests.robot b/test/tests/_old/DB2SQL_DB_Tests.robot index 46e50f5e..a66bdaac 100644 --- a/test/tests/_old/DB2SQL_DB_Tests.robot +++ b/test/tests/_old/DB2SQL_DB_Tests.robot @@ -96,4 +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; + 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 c7f16d07..59604851 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,94 @@ 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 diff --git a/test/tests/_old/PostgreSQL_DB_Tests.robot b/test/tests/_old/PostgreSQL_DB_Tests.robot index d77c106d..5cbc3f11 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 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 diff --git a/test/tests/common_tests/aliased_connection.robot b/test/tests/common_tests/aliased_connection.robot new file mode 100644 index 00000000..e61b3904 --- /dev/null +++ b/test/tests/common_tests/aliased_connection.robot @@ -0,0 +1,132 @@ +*** 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 + +Test Setup Connect, Create Some Data And Disconnect +Test Teardown Connect, Clean Up Data And Disconnect + + +*** 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 ValueError: Alias 'second' not found in existing connections. + ... 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 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 + +Use Last Connected If Not Alias Provided + 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 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 person alias=aliased_conn + + +*** 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 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}