Skip to content

Commit 6780772

Browse files
authored
Merge pull request #206 from MarketSquare/parse-script-improvements
Fix #184 Parse script improvements
2 parents 03f39f9 + 8292df2 commit 6780772

9 files changed

Lines changed: 203 additions & 64 deletions

src/DatabaseLibrary/query.py

Lines changed: 90 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414

1515
import inspect
16+
import re
1617
import sys
1718
from typing import List, Optional
1819

@@ -208,11 +209,17 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali
208209
if cur and not sansTran:
209210
db_connection.client.rollback()
210211

211-
def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, alias: Optional[str] = None):
212+
def execute_sql_script(
213+
self, sqlScriptFileName: str, sansTran: bool = False, split: bool = True, alias: Optional[str] = None
214+
):
212215
"""
213216
Executes the content of the `sqlScriptFileName` as SQL commands. Useful for setting the database to a known
214217
state before running your tests, or clearing out your test data after running each a test.
215218
219+
SQL commands are expected to be delimited by a semicolon (';') - they will be split and executed separately.
220+
You can disable this behaviour setting the parameter `split` to _False_ -
221+
in this case the entire script content will be passed to the database module for execution.
222+
216223
Sample usage :
217224
| Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql |
218225
| Execute Sql Script | ${EXECDIR}${/}resources${/}DML-setup.sql |
@@ -221,7 +228,6 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali
221228
| Execute Sql Script | ${EXECDIR}${/}resources${/}DML-teardown.sql |
222229
| Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-teardown.sql |
223230
224-
SQL commands are expected to be delimited by a semicolon (';') - they will be executed separately.
225231
226232
For example:
227233
DELETE FROM person_employee_table;
@@ -272,76 +278,101 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali
272278
with open(sqlScriptFileName, encoding="UTF-8") as sql_file:
273279
cur = None
274280
try:
275-
statements_to_execute = []
276281
cur = db_connection.client.cursor()
277282
logger.info(f"Executing : Execute SQL Script | {sqlScriptFileName}")
278-
current_statement = ""
279-
inside_statements_group = False
280-
281-
for line in sql_file:
282-
line = line.strip()
283-
if line.startswith("#") or line.startswith("--") or line == "/":
284-
continue
285-
if line.lower().startswith("begin"):
286-
inside_statements_group = True
287-
288-
# semicolons inside the line? use them to separate statements
289-
# ... but not if they are inside a begin/end block (aka. statements group)
290-
sqlFragments = line.split(";")
291-
# no semicolons
292-
if len(sqlFragments) == 1:
293-
current_statement += line + " "
294-
continue
295-
quotes = 0
296-
# "select * from person;" -> ["select..", ""]
297-
for sqlFragment in sqlFragments:
298-
if len(sqlFragment.strip()) == 0:
283+
if not split:
284+
logger.info("Statements splitting disabled - pass entire script content to the database module")
285+
self.__execute_sql(cur, sql_file.read())
286+
else:
287+
logger.info("Splitting script file into statements...")
288+
statements_to_execute = []
289+
current_statement = ""
290+
inside_statements_group = False
291+
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
292+
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
293+
for line in sql_file:
294+
line = line.strip()
295+
if line.startswith("#") or line.startswith("--") or line == "/":
299296
continue
300-
if inside_statements_group:
301-
# if statements inside a begin/end block have semicolns,
302-
# they must persist - even with oracle
303-
sqlFragment += "; "
304-
if sqlFragment.lower() == "end; ":
305-
inside_statements_group = False
306-
elif sqlFragment.lower().startswith("begin"):
297+
298+
# check if the line matches the creating procedure regexp pattern
299+
if proc_start_pattern.match(line.lower()):
300+
inside_statements_group = True
301+
elif line.lower().startswith("begin"):
307302
inside_statements_group = True
308303

309-
# check if the semicolon is a part of the value (quoted string)
310-
quotes += sqlFragment.count("'")
311-
quotes -= sqlFragment.count("\\'")
312-
quotes -= sqlFragment.count("''")
313-
inside_quoted_string = quotes % 2 != 0
314-
if inside_quoted_string:
315-
sqlFragment += ";" # restore the semicolon
316-
317-
current_statement += sqlFragment
318-
if not inside_statements_group and not inside_quoted_string:
319-
statements_to_execute.append(current_statement.strip())
320-
current_statement = ""
321-
quotes = 0
322-
323-
current_statement = current_statement.strip()
324-
if len(current_statement) != 0:
325-
statements_to_execute.append(current_statement)
326-
327-
for statement in statements_to_execute:
328-
logger.info(f"Executing statement from script file: {statement}")
329-
omit_semicolon = not statement.lower().endswith("end;")
330-
self.__execute_sql(cur, statement, omit_semicolon)
304+
# semicolons inside the line? use them to separate statements
305+
# ... but not if they are inside a begin/end block (aka. statements group)
306+
sqlFragments = line.split(";")
307+
# no semicolons
308+
if len(sqlFragments) == 1:
309+
current_statement += line + " "
310+
continue
311+
quotes = 0
312+
# "select * from person;" -> ["select..", ""]
313+
for sqlFragment in sqlFragments:
314+
if len(sqlFragment.strip()) == 0:
315+
continue
316+
317+
if inside_statements_group:
318+
# if statements inside a begin/end block have semicolns,
319+
# they must persist - even with oracle
320+
sqlFragment += "; "
321+
322+
if proc_end_pattern.match(sqlFragment.lower()):
323+
inside_statements_group = False
324+
elif proc_start_pattern.match(sqlFragment.lower()):
325+
inside_statements_group = True
326+
elif sqlFragment.lower().startswith("begin"):
327+
inside_statements_group = True
328+
329+
# check if the semicolon is a part of the value (quoted string)
330+
quotes += sqlFragment.count("'")
331+
quotes -= sqlFragment.count("\\'")
332+
quotes -= sqlFragment.count("''")
333+
inside_quoted_string = quotes % 2 != 0
334+
if inside_quoted_string:
335+
sqlFragment += ";" # restore the semicolon
336+
337+
current_statement += sqlFragment
338+
if not inside_statements_group and not inside_quoted_string:
339+
statements_to_execute.append(current_statement.strip())
340+
current_statement = ""
341+
quotes = 0
342+
343+
current_statement = current_statement.strip()
344+
if len(current_statement) != 0:
345+
statements_to_execute.append(current_statement)
346+
347+
for statement in statements_to_execute:
348+
logger.info(f"Executing statement from script file: {statement}")
349+
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
350+
omit_semicolon = not line_ends_with_proc_end.search(statement.lower())
351+
self.__execute_sql(cur, statement, omit_semicolon)
331352
if not sansTran:
332353
db_connection.client.commit()
333354
finally:
334355
if cur and not sansTran:
335356
db_connection.client.rollback()
336357

337358
def execute_sql_string(
338-
self, sqlString: str, sansTran: bool = False, alias: Optional[str] = None, parameters: Optional[List] = None
359+
self,
360+
sqlString: str,
361+
sansTran: bool = False,
362+
omitTrailingSemicolon: Optional[bool] = None,
363+
alias: Optional[str] = None,
364+
parameters: Optional[List] = None,
339365
):
340366
"""
341-
Executes the sqlString as SQL commands. Useful to pass arguments to your sql.
342-
SQL commands are expected to be delimited by a semicolon (';').
367+
Executes the ``sqlString`` as a single SQL command.
343368
344-
Use optional `sansTran` to run command without an explicit transaction commit or rollback:
369+
Use optional ``sansTran`` to run command without an explicit transaction commit or rollback.
370+
371+
Use optional ``omitTrailingSemicolon`` parameter for explicit instruction,
372+
if the trailing semicolon (;) at the SQL string end should be removed or not:
373+
- Some database modules (e.g. Oracle) throw an exception, if you leave a semicolon at the string end
374+
- However, there are exceptional cases, when you need it even for Oracle - e.g. at the end of a PL/SQL block.
375+
- If not specified, it's decided based on the current database module in use. For Oracle, the semicolon is removed by default.
345376
346377
Use optional ``alias`` parameter to specify what connection should be used for the query if you have more
347378
than one connection open.
@@ -353,6 +384,7 @@ def execute_sql_string(
353384
| Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table |
354385
| Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | alias=my_alias |
355386
| Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | sansTran=True |
387+
| Execute Sql String | CREATE PROCEDURE proc AS BEGIN DBMS_OUTPUT.PUT_LINE('Hello!'); END; | omitTrailingSemicolon=False |
356388
| @{parameters} | Create List | person_employee_table |
357389
| Execute Sql String | SELECT * FROM %s | parameters=${parameters} |
358390
"""
@@ -361,7 +393,7 @@ def execute_sql_string(
361393
try:
362394
cur = db_connection.client.cursor()
363395
logger.info(f"Executing : Execute SQL String | {sqlString}")
364-
self.__execute_sql(cur, sqlString, parameters=parameters)
396+
self.__execute_sql(cur, sqlString, omit_trailing_semicolon=omitTrailingSemicolon, parameters=parameters)
365397
if not sansTran:
366398
db_connection.client.commit()
367399
finally:

test/readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ See the folder `.github/workflows`
4040

4141
## Microsoft SQL Server
4242
- https://hub.docker.com/_/microsoft-mssql-server
43-
- docker run --rm --name mssql -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=MyPass1234!" -p 1433:1433 -d mcr.microsoft.com/mssql/server
43+
- docker run --rm --name mssql -e ACCEPT_EULA=Y -e MSSQL_SA_PASSWORD='MyPass1234!' -p 1433:1433 -d mcr.microsoft.com/mssql/server
4444
--> login and create DB:
4545
- docker exec -it mssql bash
4646
- /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'MyPass1234!'

test/resources/create_stored_procedure_mssql.sql renamed to test/resources/create_stored_procedures_mssql.sql

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,18 @@ BEGIN
3333
SELECT FIRST_NAME FROM person;
3434
SELECT LAST_NAME FROM person;
3535
RETURN;
36+
END;
37+
38+
DROP PROCEDURE IF EXISTS check_condition;
39+
CREATE PROCEDURE check_condition
40+
AS
41+
DECLARE @v_condition BIT = 1;
42+
IF @v_condition = 1
43+
BEGIN
44+
PRINT 'Condition is true';
45+
END
46+
ELSE
47+
BEGIN
48+
PRINT 'Condition is false';
49+
END
3650
END;

test/resources/create_stored_procedure_mysql.sql renamed to test/resources/create_stored_procedures_mysql.sql

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,15 @@ CREATE PROCEDURE get_all_first_and_second_names()
2727
BEGIN
2828
SELECT FIRST_NAME FROM person;
2929
SELECT LAST_NAME FROM person;
30-
END;
30+
END;
31+
32+
DROP PROCEDURE IF EXISTS check_condition;
33+
CREATE PROCEDURE check_condition()
34+
BEGIN
35+
DECLARE v_condition BOOLEAN DEFAULT TRUE;
36+
IF v_condition THEN
37+
SELECT 'Condition is true' AS Result;
38+
ELSE
39+
SELECT 'Condition is false' AS Result;
40+
END IF;
41+
END

test/resources/create_stored_procedures_oracle.sql

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,15 @@ OPEN first_names_cursor for
2929
SELECT FIRST_NAME FROM person;
3030
OPEN second_names_cursor for
3131
SELECT LAST_NAME FROM person;
32-
END;
32+
END;
33+
34+
CREATE OR REPLACE PROCEDURE
35+
check_condition AS
36+
v_condition BOOLEAN := TRUE;
37+
BEGIN
38+
IF v_condition THEN
39+
DBMS_OUTPUT.PUT_LINE('Condition is true');
40+
ELSE
41+
DBMS_OUTPUT.PUT_LINE('Condition is false');
42+
END IF;
43+
END check_condition;

test/resources/create_stored_procedure_postgres.sql renamed to test/resources/create_stored_procedures_postgres.sql

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,23 @@ RETURN NEXT result1;
4949
OPEN result2 FOR SELECT LAST_NAME FROM person;
5050
RETURN NEXT result2;
5151
END
52+
';
53+
54+
DROP ROUTINE IF EXISTS check_condition;
55+
CREATE FUNCTION
56+
check_condition()
57+
RETURNS VOID
58+
LANGUAGE plpgsql
59+
AS
60+
'
61+
DECLARE
62+
v_condition BOOLEAN := TRUE;
63+
v_res BOOLEAN := TRUE;
64+
BEGIN
65+
IF v_condition THEN
66+
v_res := TRUE;
67+
ELSE
68+
v_res := FALSE;
69+
END IF;
70+
END
5271
';

test/tests/common_tests/stored_procedures.robot

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,21 @@ Procedure Returns Multiple Result Sets
8888
Should Be Equal ${second result set}[0][0] See
8989
Should Be Equal ${second result set}[1][0] Schneider
9090

91+
Procedure With IF/ELSE Block
92+
Call Stored Procedure check_condition
93+
9194

9295
*** Keywords ***
9396
Create And Fill Tables And Stored Procedures
9497
Create Person Table And Insert Data
9598
IF "${DB_MODULE}" in ["oracledb", "cx_Oracle"]
9699
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_oracle.sql
97100
ELSE IF "${DB_MODULE}" in ["pymysql"]
98-
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedure_mysql.sql
101+
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_mysql.sql
99102
ELSE IF "${DB_MODULE}" in ["psycopg2", "psycopg3"]
100-
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedure_postgres.sql
103+
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_postgres.sql
101104
ELSE IF "${DB_MODULE}" in ["pymssql"]
102-
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedure_mssql.sql
105+
Execute SQL Script ${CURDIR}/../../resources/create_stored_procedures_mssql.sql
103106
ELSE
104107
Skip Don't know how to create stored procedures for '${DB_MODULE}'
105108
END
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
*** Settings ***
2+
Documentation Tests for the parameter _omitTrailingSemicolon_ in the keyword
3+
... _Execute SQL String_ - special for the issue #184:
4+
... https://github.com/MarketSquare/Robotframework-Database-Library/issues/184
5+
... The _PLSQL BLOCK_ is most likely valid for Oracle DB only.
6+
7+
Resource ../../resources/common.resource
8+
Suite Setup Connect To DB
9+
Suite Teardown Disconnect From Database
10+
Test Setup Create Person Table And Insert Data
11+
Test Teardown Drop Tables Person And Foobar
12+
13+
14+
*** Variables ***
15+
${NORMAL QUERY} SELECT * FROM person;
16+
${PLSQL BLOCK} DECLARE ERRCODE NUMBER; ERRMSG VARCHAR2(200); BEGIN DBMS_OUTPUT.PUT_LINE('Hello!'); END;
17+
18+
19+
*** Test Cases ***
20+
Explicitely Omit Semicolon
21+
[Documentation] Check if it works for Oracle - explicitely omitting the semicolon
22+
... is equal to the default behaviour, otherwise oracle_db throws an error
23+
Execute Sql String ${NORMAL QUERY} omitTrailingSemicolon=True
24+
25+
Explicitely Dont't Omit Semicolon
26+
[Documentation] Check if it works for Oracle - it throws an error without a semicolon
27+
Execute Sql String ${PLSQL BLOCK} omitTrailingSemicolon=False
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
*** Settings ***
2+
Documentation Tests for the parameter _split_ in the keyword
3+
... _Execute SQL Script_ - special for the issue #184:
4+
... https://github.com/MarketSquare/Robotframework-Database-Library/issues/184
5+
6+
Resource ../../resources/common.resource
7+
Suite Setup Connect To DB
8+
Suite Teardown Disconnect From Database
9+
Test Setup Create Person Table
10+
Test Teardown Drop Tables Person And Foobar
11+
12+
13+
*** Test Cases ***
14+
Split Commands
15+
[Documentation] Such a simple script works always,
16+
... just check if the logs if the parameter value was processed properly
17+
Execute Sql Script ${CURDIR}/../../resources/insert_data_in_person_table.sql split=True
18+
19+
Don't Split Commands
20+
[Documentation] Running such a script as a single statement works for PostgreSQL,
21+
... but fails in Oracle. Check in the logs if the splitting was disabled.
22+
Execute Sql Script ${CURDIR}/../../resources/insert_data_in_person_table.sql split=False

0 commit comments

Comments
 (0)