diff --git a/src/DatabaseLibrary/assertion.py b/src/DatabaseLibrary/assertion.py index 64a8723..7e0862f 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. diff --git a/src/DatabaseLibrary/query.py b/src/DatabaseLibrary/query.py index 2da52fe..01a4842 100644 --- a/src/DatabaseLibrary/query.py +++ b/src/DatabaseLibrary/query.py @@ -25,11 +25,10 @@ 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 - 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) @@ -69,7 +73,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] @@ -79,10 +83,9 @@ 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. + 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) @@ -115,7 +123,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) @@ -124,10 +132,9 @@ 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. + 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 | """ @@ -154,7 +166,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)): @@ -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) @@ -331,10 +341,9 @@ 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. + 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) @@ -356,7 +370,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: @@ -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) @@ -507,7 +519,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 +531,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) diff --git a/test/tests/common_tests/query_params.robot b/test/tests/common_tests/query_params.robot new file mode 100644 index 0000000..839059b --- /dev/null +++ b/test/tests/common_tests/query_params.robot @@ -0,0 +1,67 @@ +*** Settings *** +Documentation Keywords with query params as seperate arguments work across all databases. + +Resource ../../resources/common.resource + +Suite Setup Connect To DB And Build Query +Suite Teardown Disconnect From Database +Test Setup Create Person Table And Insert Data +Test Teardown Drop Tables Person And Foobar + + +*** Variables *** +@{PARAMS} Franz Allan + + +*** Keywords *** +Connect To DB And Build Query + Connect To DB + Build Query String With Params + +Build Query String With Params + ${sql}= Set Variable SELECT id FROM person WHERE FIRST_NAME= + IF "${DB_MODULE}" in ["oracledb", "cx_Oracle"] + ${sql}= Catenate ${sql} :id + ELSE IF "${DB_MODULE}" in ["sqlite3", "pyodbc"] + ${sql}= Catenate ${sql} ? + ELSE + ${sql}= Catenate ${sql} %s + END + Set Suite Variable ${QUERY} ${sql} + + +*** Test Cases *** +Query + ${out}= Query ${QUERY} parameters=${PARAMS} + Length Should Be ${out} 1 + +Row Count + ${out}= Row Count ${QUERY} parameters=${PARAMS} + Should Be Equal As Strings ${out} 1 + +Description + ${out}= Description ${QUERY} parameters=${PARAMS} + Length Should Be ${out} 1 + +Execute SQL String + Execute Sql String ${QUERY} parameters=${PARAMS} + +Check If Exists In DB + Check If Exists In Database ${QUERY} parameters=${PARAMS} + +Check If Not Exists In DB + @{Wrong params}= Create List Joe + Check If Not Exists In Database ${QUERY} parameters=${Wrong params} + +Row Count is 0 + @{Wrong params}= Create List Joe + Row Count is 0 ${QUERY} parameters=${Wrong params} + +Row Count is Equal to X + Row Count is Equal to X ${QUERY} 1 parameters=${PARAMS} + +Row Count is Less Than X + Row Count is Less Than X ${QUERY} 5 parameters=${PARAMS} + +Row Count is Greater Than X + Row Count is Greater Than X ${QUERY} 0 parameters=${PARAMS}