From 7df2cca017dd24ca3ba5c8c1012aa923e92c72a6 Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Fri, 10 Nov 2023 13:39:55 +0100 Subject: [PATCH 1/9] Fix and improve error handling for missing or invalid configuration file --- .github/workflows/unit_tests.yml | 59 ++++++++++++++++++++ src/DatabaseLibrary/connection_manager.py | 56 +++++++++++++------ test/tests/__init__.py | 0 test/tests/utests/__init__.py | 0 test/tests/utests/test_connection_manager.py | 48 ++++++++++++++++ test/tests/utests/test_data/alias.cfg | 2 + test/tests/utests/test_data/empty.cfg | 0 test/tests/utests/test_data/no_option.cfg | 3 + 8 files changed, 151 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/unit_tests.yml create mode 100644 test/tests/__init__.py create mode 100644 test/tests/utests/__init__.py create mode 100644 test/tests/utests/test_connection_manager.py create mode 100644 test/tests/utests/test_data/alias.cfg create mode 100644 test/tests/utests/test_data/empty.cfg create mode 100644 test/tests/utests/test_data/no_option.cfg diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 00000000..1f284dc8 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,59 @@ +--- +# This workflow will install Python dependencies +# and run unit tests for given OSes + +name: Unit tests + +on: [push, pull_request] + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: 'ubuntu-latest' + python-version: '3.7' + rf-version: '3.2.2' + - os: 'ubuntu-latest' + python-version: '3.8' + rf-version: '4.1.3' + - os: 'ubuntu-latest' + python-version: '3.9' + rf-version: '5.0.1' + - os: 'ubuntu-latest' + python-version: '3.10' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.11' + rf-version: '6.1.1' + - os: 'ubuntu-latest' + python-version: '3.12' + rf-version: '7.0a1' + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install robotframework==${{ matrix.rf-version }} coverage pytest + pip install . + + - name: Run unit tests with coverage + run: + coverage run -m pytest + + - name: Codecov + uses: codecov/codecov-action@v3 + with: + name: ${{ matrix.python-version }}-${{ matrix.os }}-${{ matrix.rf-version }} diff --git a/src/DatabaseLibrary/connection_manager.py b/src/DatabaseLibrary/connection_manager.py index 9ae23d60..18c042b4 100644 --- a/src/DatabaseLibrary/connection_manager.py +++ b/src/DatabaseLibrary/connection_manager.py @@ -13,14 +13,11 @@ # limitations under the License. import importlib +from configparser import ConfigParser, NoOptionError, NoSectionError from dataclasses import dataclass +from pathlib import Path from typing import Any, Dict, Optional -try: - import ConfigParser -except: - import configparser as ConfigParser - from robot.api import logger @@ -81,6 +78,35 @@ def __iter__(self): return iter(self._connections.values()) +class ConfigReader: + def __init__(self, config_file: Optional[str], alias: str): + if config_file is None: + config_file = "./resources/db.cfg" + self.alias = alias + self.config = self._load_config(config_file) + + @staticmethod + def _load_config(config_file: str) -> Optional[ConfigParser]: + config_path = Path(config_file) + if not config_path.exists(): + return None + config = ConfigParser() + config.read([config_path]) + return config + + def get(self, param: str) -> str: + if self.config is None: + raise ValueError(f"Required '{param}' parameter was not provided in keyword arguments.") from None + try: + return self.config.get(self.alias, param) + except NoSectionError: + raise ValueError(f"Configuration file does not have [{self.alias}] section.") from None + except NoOptionError: + raise ValueError( + f"Required '{param}' parameter missing in both keyword arguments and configuration file." + ) from None + + class ConnectionManager: """ Connection Manager handles the connection & disconnection to the database. @@ -153,18 +179,14 @@ def connect_to_database( | # uses explicit `dbapiModuleName` and `dbName` but uses the `dbUsername` and `dbPassword` in './resources/db.cfg' | | Connect To Database | psycopg2 | my_db_test | """ - - if dbConfigFile is None: - dbConfigFile = "./resources/db.cfg" - config = ConfigParser.ConfigParser() - config.read([dbConfigFile]) - - 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")) + config = ConfigReader(dbConfigFile, alias) + + dbapiModuleName = dbapiModuleName or config.get("dbapiModuleName") + dbName = dbName or config.get("dbName") + dbUsername = dbUsername or config.get("dbUsername") + dbPassword = dbPassword if dbPassword is not None else config.get("dbPassword") + dbHost = dbHost or config.get("dbHost") or "localhost" + dbPort = int(dbPort if dbPort is not None else config.get("dbPort")) if dbapiModuleName == "excel" or dbapiModuleName == "excelrw": db_api_module_name = "pyodbc" diff --git a/test/tests/__init__.py b/test/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/__init__.py b/test/tests/utests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_connection_manager.py b/test/tests/utests/test_connection_manager.py new file mode 100644 index 00000000..bbc9ea26 --- /dev/null +++ b/test/tests/utests/test_connection_manager.py @@ -0,0 +1,48 @@ +import re +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from DatabaseLibrary.connection_manager import ConnectionManager + +TEST_DATA = Path(__file__).parent / "test_data" + + +class TestConnectWithConfigFile: + def test_connect_with_empty_config(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "empty.cfg") + with pytest.raises(ValueError, match=re.escape("Configuration file does not have [default] section.")): + conn_manager.connect_to_database("my_client", dbConfigFile=config_path) + + def test_connect_no_params_no_config(self): + conn_manager = ConnectionManager() + with pytest.raises(ValueError, match="Required 'dbName' parameter was not provided in keyword arguments."): + conn_manager.connect_to_database("my_client") + + def test_connect_missing_option(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "no_option.cfg") + with pytest.raises( + ValueError, + match="Required 'dbPassword' parameter missing in both keyword arguments and configuration file.", + ): + conn_manager.connect_to_database("my_client", dbConfigFile=config_path) + + def test_aliased_section(self): + conn_manager = ConnectionManager() + config_path = str(TEST_DATA / "alias.cfg") + with patch("importlib.import_module", new=MagicMock()) as client: + conn_manager.connect_to_database( + "my_client", + dbUsername="name", + dbPassword="password", + dbHost="host", + dbPort=0, + dbConfigFile=config_path, + alias="alias2", + ) + client.return_value.connect.assert_called_with( + database="example", user="name", password="password", host="host", port=0 + ) diff --git a/test/tests/utests/test_data/alias.cfg b/test/tests/utests/test_data/alias.cfg new file mode 100644 index 00000000..06d0431c --- /dev/null +++ b/test/tests/utests/test_data/alias.cfg @@ -0,0 +1,2 @@ +[alias2] +dbName = example diff --git a/test/tests/utests/test_data/empty.cfg b/test/tests/utests/test_data/empty.cfg new file mode 100644 index 00000000..e69de29b diff --git a/test/tests/utests/test_data/no_option.cfg b/test/tests/utests/test_data/no_option.cfg new file mode 100644 index 00000000..4e0db9e8 --- /dev/null +++ b/test/tests/utests/test_data/no_option.cfg @@ -0,0 +1,3 @@ +[default] +dbName = example +dbUsername = example \ No newline at end of file From 1a9987dbdd98eed70670e91496750f64c37e0368 Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:02:22 +0200 Subject: [PATCH 2/9] Update query.py add parameters argument --- src/DatabaseLibrary/query.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index 2da52fe8..dc483c0b 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -25,7 +25,7 @@ class Query: """ def query( - self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None + self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None, parameters: Optional[List] = None ): """ Uses the input ``selectStatement`` to query for the values that will be returned as a list of tuples. Set @@ -79,7 +79,7 @@ def query( if cur and not sansTran: db_connection.client.rollback() - def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): + def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. @@ -124,7 +124,7 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona if cur and not sansTran: db_connection.client.rollback() - def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None): + def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. @@ -331,7 +331,7 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali if cur and not sansTran: db_connection.client.rollback() - def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None): + def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. @@ -507,7 +507,7 @@ def call_stored_procedure( if cur and not sansTran: db_connection.client.rollback() - def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Optional[bool] = None): + def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Optional[bool] = None, parameters: Optional[List] = None): """ Runs the `sql_statement` using `cur` as Cursor object. Use `omit_trailing_semicolon` parameter (bool) for explicit instruction, @@ -519,5 +519,7 @@ def __execute_sql(self, cur, sql_statement: str, omit_trailing_semicolon: Option omit_trailing_semicolon = self.omit_trailing_semicolon if omit_trailing_semicolon: sql_statement = sql_statement.rstrip(";") + if parameters is None: + parameters = [] logger.debug(f"Executing sql: {sql_statement}") - return cur.execute(sql_statement) + return cur.execute(sql_statement, parameters) From 402664c42acf1bf41555ff031299ae764bd71798 Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:15:47 +0200 Subject: [PATCH 3/9] Update query.py pass parameters argument to execute function --- src/DatabaseLibrary/query.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index dc483c0b..4c4bc2bb 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -69,7 +69,7 @@ def query( try: cur = db_connection.client.cursor() logger.info(f"Executing : Query | {selectStatement} ") - self.__execute_sql(cur, selectStatement) + self.__execute_sql(cur, selectStatement, parameters=parameters) all_rows = cur.fetchall() if returnAsDict: col_names = [c[0] for c in cur.description] @@ -115,7 +115,7 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona try: cur = db_connection.client.cursor() logger.info(f"Executing : Row Count | {selectStatement}") - self.__execute_sql(cur, selectStatement) + self.__execute_sql(cur, selectStatement, parameters=parameters) data = cur.fetchall() if db_connection.module_name in ["sqlite3", "ibm_db", "ibm_db_dbi", "pyodbc"]: return len(data) @@ -154,7 +154,7 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio try: cur = db_connection.client.cursor() logger.info("Executing : Description | {selectStatement}") - self.__execute_sql(cur, selectStatement) + self.__execute_sql(cur, selectStatement, parameters=parameters) description = list(cur.description) if sys.version_info[0] < 3: for row in range(0, len(description)): @@ -356,7 +356,7 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Opti try: cur = db_connection.client.cursor() logger.info(f"Executing : Execute SQL String | {sqlString}") - self.__execute_sql(cur, sqlString) + self.__execute_sql(cur, sqlString, parameters=parameters) if not sansTran: db_connection.client.commit() finally: From 3f94ad43d8758181b3323f501baa2229372e599b Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:32:00 +0200 Subject: [PATCH 4/9] Update basic_tests.robot added test using parameters argument --- test/tests/common_tests/basic_tests.robot | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/tests/common_tests/basic_tests.robot b/test/tests/common_tests/basic_tests.robot index 2fe17bc3..2f287726 100644 --- a/test/tests/common_tests/basic_tests.robot +++ b/test/tests/common_tests/basic_tests.robot @@ -16,6 +16,10 @@ SQL Statement Ending With Semicolon Works SQL Statement Ending Without Semicolon Works Query SELECT * FROM person; +SQL Statement With Parameters Works + ${output}= Query SELECT * FROM person WHERE id < ? parameters=[1] + Length Should Be ${output} 1 + Create Person Table [Setup] Log No setup for this test ${output}= Create Person Table From 16a281e51ffea0106ceec745deaaa8523f406390 Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:55:05 +0200 Subject: [PATCH 5/9] Update basic_tests.robot use correct list parameter --- test/tests/common_tests/basic_tests.robot | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/tests/common_tests/basic_tests.robot b/test/tests/common_tests/basic_tests.robot index 2f287726..386c5ae6 100644 --- a/test/tests/common_tests/basic_tests.robot +++ b/test/tests/common_tests/basic_tests.robot @@ -17,7 +17,8 @@ SQL Statement Ending Without Semicolon Works Query SELECT * FROM person; SQL Statement With Parameters Works - ${output}= Query SELECT * FROM person WHERE id < ? parameters=[1] + @{params}= Create List 2 + ${output}= Query SELECT * FROM person WHERE id < ? parameters=${params} Length Should Be ${output} 1 Create Person Table From 5f2c0226fcb4e45fd4928d0a2f0cee1b62fc9043 Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Mon, 14 Aug 2023 13:42:47 +0200 Subject: [PATCH 6/9] Update basic_tests.robot try different syntax for postgres, oracle etc. --- test/tests/common_tests/basic_tests.robot | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/tests/common_tests/basic_tests.robot b/test/tests/common_tests/basic_tests.robot index 386c5ae6..8f1f28ec 100644 --- a/test/tests/common_tests/basic_tests.robot +++ b/test/tests/common_tests/basic_tests.robot @@ -18,7 +18,11 @@ SQL Statement Ending Without Semicolon Works SQL Statement With Parameters Works @{params}= Create List 2 - ${output}= Query SELECT * FROM person WHERE id < ? parameters=${params} + TRY + ${output}= Query SELECT * FROM person WHERE id < ? parameters=${params} + EXCEPT + ${output}= Query SELECT * FROM person WHERE id < :id parameters=${params} + END Length Should Be ${output} 1 Create Person Table From 6ce795c32c215526ec3590e9b6376ca86921dc94 Mon Sep 17 00:00:00 2001 From: carnegiemedal <126577777+carnegiemedal@users.noreply.github.com> Date: Mon, 14 Aug 2023 14:09:11 +0200 Subject: [PATCH 7/9] Update basic_tests.robot extra syntax alternative for postgres, pymsql --- test/tests/common_tests/basic_tests.robot | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/tests/common_tests/basic_tests.robot b/test/tests/common_tests/basic_tests.robot index 8f1f28ec..4db398c1 100644 --- a/test/tests/common_tests/basic_tests.robot +++ b/test/tests/common_tests/basic_tests.robot @@ -18,11 +18,15 @@ SQL Statement Ending Without Semicolon Works SQL Statement With Parameters Works @{params}= Create List 2 - TRY - ${output}= Query SELECT * FROM person WHERE id < ? parameters=${params} - EXCEPT + + IF "${DB_MODULE}" in ["oracledb"] ${output}= Query SELECT * FROM person WHERE id < :id parameters=${params} + ELSE IF "${DB_MODULE}" in ["sqlite3", "pyodbc"] + ${output}= Query SELECT * FROM person WHERE id < ? parameters=${params} + ELSE + ${output}= Query SELECT * FROM person WHERE id < %s parameters=${params} END + Length Should Be ${output} 1 Create Person Table From 8902334e6b7748ba085e13f74b21f2d87ecd3cf4 Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Sun, 19 Nov 2023 16:56:23 +0100 Subject: [PATCH 8/9] Update documentation with parameters examples --- src/DatabaseLibrary/query.py | 52 ++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index 4c4bc2bb..01a48428 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -28,8 +28,7 @@ def query( self, selectStatement: str, sansTran: bool = False, returnAsDict: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. + Uses the input ``selectStatement`` to query for the values that will be returned as a list of tuples. 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 @@ -61,7 +60,12 @@ def query( And get the following See, Franz Allan - Using optional ``sansTran`` to run command without an explicit transaction commit or rollback: + Use optional ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client): + | parameters | Create List | person | + | Query | SELECT * FROM %s | parameters=${parameters} | + + Use 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) @@ -81,8 +85,7 @@ def query( def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -107,7 +110,12 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona 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: + Use optional ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client): + | parameters | Create List | person | + | ${rowCount} | Row Count | SELECT * FROM %s | parameters=${parameters} | + + Use 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) @@ -126,8 +134,7 @@ def row_count(self, selectStatement: str, sansTran: bool = False, alias: Optiona def description(self, selectStatement: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. For example, given we have a table `person` with the following data: | id | first_name | last_name | @@ -146,6 +153,11 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio Use optional ``alias`` parameter to specify what connection should be used for the query if you have more than one connection open. + Use optional ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client): + | parameters | Create List | person | + | ${desc} | Description | SELECT * FROM %s | parameters=${parameters} | + Using optional `sansTran` to run command without an explicit transaction commit or rollback: | @{queryResults} | Description | SELECT * FROM person | True | """ @@ -166,8 +178,7 @@ def description(self, selectStatement: str, sansTran: bool = False, alias: Optio 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. + Delete all the rows within a given table. For example, given we have a table `person` in a database @@ -184,7 +195,7 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali 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: + Use 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) @@ -207,8 +218,7 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali 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. + state before running your tests, or clearing out your test data after running each a test. Sample usage : | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql | @@ -262,7 +272,7 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali 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: + Use 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) @@ -333,8 +343,7 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = 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. + Executes the sqlString as SQL commands. Useful to pass arguments to your sql. SQL commands are expected to be delimited by a semicolon (';'). @@ -348,7 +357,12 @@ def execute_sql_string(self, sqlString: str, sansTran: bool = False, alias: Opti 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: + Use optional ``parameters`` for query variable substitution (variable substitution syntax may be different + depending on the database client): + | parameters | Create List | person_employee_table | + | Execute Sql String | SELECT * FROM %s | parameters=${parameters} | + + Use 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) @@ -381,8 +395,6 @@ def call_stored_procedure( It also depends on the database, how the procedure returns the values - as params or as result sets. E.g. calling a procedure in *PostgreSQL* returns even a single value of an OUT param as a result set. - Set optional input `sansTran` to True to run command without an explicit transaction commit or rollback. - Simple example: | @{Params} = | Create List | Jerry | out_second_name | | @{Param values} @{Result sets} = | Call Stored Procedure | Get_second_name | ${Params} | @@ -404,7 +416,7 @@ def call_stored_procedure( 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: + Use 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) From 2981b161d2b9494df0b4ad8e48190d6f868fc9ee Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Mon, 20 Nov 2023 17:59:12 +0100 Subject: [PATCH 9/9] Add missing parameters arg to assertions keywords --- src/DatabaseLibrary/assertion.py | 40 +++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 64a8723a..7e0862fc 100644 --- a/src/DatabaseLibrary/assertion.py +++ b/src/DatabaseLibrary/assertion.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional +from typing import List, Optional from robot.api import logger @@ -22,7 +22,12 @@ class Assertion: """ def check_if_exists_in_database( - self, selectStatement: str, sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None + self, + selectStatement: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ Check if any row would be returned by given the input ``selectStatement``. If there are no results, then this will @@ -43,13 +48,18 @@ def check_if_exists_in_database( | 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): + if not self.query(selectStatement, sansTran, alias=alias, parameters=parameters): raise AssertionError( 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 + self, + selectStatement: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ This is the negation of `check_if_exists_in_database`. @@ -71,14 +81,19 @@ def check_if_not_exists_in_database( | 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) + query_results = self.query(selectStatement, sansTran, alias=alias, parameters=parameters) 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, alias: Optional[str] = None + self, + selectStatement: str, + sansTran: bool = False, + msg: Optional[str] = None, + alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ Check if any rows are returned from the submitted ``selectStatement``. If there are, then this will throw an @@ -99,7 +114,7 @@ def row_count_is_0( | 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) + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) if num_rows > 0: raise AssertionError(msg or f"Expected 0 rows, but {num_rows} were returned from: '{selectStatement}'") @@ -110,6 +125,7 @@ def row_count_is_equal_to_x( sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ Check if the number of rows returned from ``selectStatement`` is equal to the value submitted. If not, then this @@ -129,7 +145,7 @@ def row_count_is_equal_to_x( | 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) + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) if num_rows != int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected {numRows} rows, but {num_rows} were returned from: '{selectStatement}'" @@ -142,6 +158,7 @@ def row_count_is_greater_than_x( sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ Check if the number of rows returned from ``selectStatement`` is greater than the value submitted. If not, then @@ -161,7 +178,7 @@ def row_count_is_greater_than_x( | 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) + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) if num_rows <= int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected more than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" @@ -174,6 +191,7 @@ def row_count_is_less_than_x( sansTran: bool = False, msg: Optional[str] = None, alias: Optional[str] = None, + parameters: Optional[List] = None, ): """ Check if the number of rows returned from ``selectStatement`` is less than the value submitted. If not, then this @@ -194,7 +212,7 @@ def row_count_is_less_than_x( """ logger.info(f"Executing : Row Count Is Less Than X | {selectStatement} | {numRows}") - num_rows = self.row_count(selectStatement, sansTran, alias=alias) + num_rows = self.row_count(selectStatement, sansTran, alias=alias, parameters=parameters) if num_rows >= int(numRows.encode("ascii")): raise AssertionError( msg or f"Expected less than {numRows} rows, but {num_rows} were returned from '{selectStatement}'" @@ -204,7 +222,7 @@ 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. + Check if the given table exists in the database. Set optional input ``sansTran`` to True to run command without an explicit transaction commit or rollback.