Skip to content

Commit 4e94870

Browse files
authored
Merge pull request #248 from MarketSquare/split_script_feature
New keyword "Split SQL Script" and external script parser
2 parents bfd0745 + 31123c9 commit 4e94870

File tree

4 files changed

+121
-69
lines changed

4 files changed

+121
-69
lines changed

pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
requires = [
33
"setuptools>=61.0",
44
"robotframework>=5.0.1",
5-
"robotframework-assertion-engine"
5+
"robotframework-assertion-engine",
6+
"sqlparse"
67
]
78
build-backend = "setuptools.build_meta"
89

@@ -15,7 +16,8 @@ readme = "README.md"
1516
requires-python = ">=3.8.1"
1617
dependencies = [
1718
"robotframework>=5.0.1",
18-
"robotframework-assertion-engine"
19+
"robotframework-assertion-engine",
20+
"sqlparse"
1921
]
2022
classifiers = [
2123
"Programming Language :: Python :: 3",

src/DatabaseLibrary/query.py

Lines changed: 90 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import sys
1919
from typing import List, Optional, Tuple
2020

21+
import sqlparse
2122
from robot.api import logger
2223
from robot.utils.dotdict import DotDict
2324

@@ -263,6 +264,7 @@ def execute_sql_script(
263264
no_transaction: bool = False,
264265
alias: Optional[str] = None,
265266
split: bool = True,
267+
external_parser=False,
266268
*,
267269
sqlScriptFileName: Optional[str] = None,
268270
sansTran: Optional[bool] = None,
@@ -274,6 +276,8 @@ def execute_sql_script(
274276
Set ``split`` to _False_ to disable this behavior - in this case the entire script content
275277
will be passed to the database module for execution as a single command.
276278
279+
Set `external_parser` to _True_ to use the external `sqlparse` library for splitting the script.
280+
277281
Set ``no_transaction`` to _True_ to run command without explicit transaction commit
278282
or rollback in case of error.
279283
See `Commit behavior` for details.
@@ -293,84 +297,104 @@ def execute_sql_script(
293297
| Execute SQL Script | insert_data_in_person_table.sql | split=False |
294298
"""
295299
db_connection = self.connection_store.get_connection(alias)
296-
with open(script_path, encoding="UTF-8") as sql_file:
297-
cur = None
298-
try:
299-
cur = db_connection.client.cursor()
300-
if not split:
300+
cur = None
301+
try:
302+
cur = db_connection.client.cursor()
303+
if not split:
304+
with open(script_path, encoding="UTF-8") as sql_file:
301305
logger.info("Statements splitting disabled - pass entire script content to the database module")
302306
self._execute_sql(
303307
cur,
304308
sql_file.read(),
305309
omit_trailing_semicolon=db_connection.omit_trailing_semicolon,
306310
)
307-
else:
308-
logger.info("Splitting script file into statements...")
309-
statements_to_execute = []
310-
current_statement = ""
311-
inside_statements_group = False
312-
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
311+
else:
312+
statements_to_execute = self.split_sql_script(script_path, external_parser=external_parser)
313+
for statement in statements_to_execute:
313314
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
314-
for line in sql_file:
315-
line = line.strip()
316-
if line.startswith("#") or line.startswith("--") or line == "/":
315+
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
316+
omit_semicolon = not line_ends_with_proc_end.search(statement.lower())
317+
self._execute_sql(cur, statement, omit_semicolon)
318+
self._commit_if_needed(db_connection, no_transaction)
319+
except Exception as e:
320+
self._rollback_and_raise(db_connection, no_transaction, e)
321+
322+
def split_sql_script(
323+
self,
324+
script_path: str,
325+
external_parser=False,
326+
):
327+
"""
328+
Splits the content of the SQL script file loaded from `script_path` into individual SQL commands
329+
and returns them as a list of strings.
330+
SQL commands are expected to be delimited by a semicolon (';').
331+
332+
Set `external_parser` to _True_ to use the external `sqlparse` library.
333+
"""
334+
with open(script_path, encoding="UTF-8") as sql_file:
335+
logger.info("Splitting script file into statements...")
336+
statements_to_execute = []
337+
if external_parser:
338+
statements_to_execute = sqlparse.split(sql_file.read())
339+
else:
340+
current_statement = ""
341+
inside_statements_group = False
342+
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
343+
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
344+
for line in sql_file:
345+
line = line.strip()
346+
if line.startswith("#") or line.startswith("--") or line == "/":
347+
continue
348+
349+
# check if the line matches the creating procedure regexp pattern
350+
if proc_start_pattern.match(line.lower()):
351+
inside_statements_group = True
352+
elif line.lower().startswith("begin"):
353+
inside_statements_group = True
354+
355+
# semicolons inside the line? use them to separate statements
356+
# ... but not if they are inside a begin/end block (aka. statements group)
357+
sqlFragments = line.split(";")
358+
# no semicolons
359+
if len(sqlFragments) == 1:
360+
current_statement += line + " "
361+
continue
362+
quotes = 0
363+
# "select * from person;" -> ["select..", ""]
364+
for sqlFragment in sqlFragments:
365+
if len(sqlFragment.strip()) == 0:
317366
continue
318367

319-
# check if the line matches the creating procedure regexp pattern
320-
if proc_start_pattern.match(line.lower()):
368+
if inside_statements_group:
369+
# if statements inside a begin/end block have semicolns,
370+
# they must persist - even with oracle
371+
sqlFragment += "; "
372+
373+
if proc_end_pattern.match(sqlFragment.lower()):
374+
inside_statements_group = False
375+
elif proc_start_pattern.match(sqlFragment.lower()):
321376
inside_statements_group = True
322-
elif line.lower().startswith("begin"):
377+
elif sqlFragment.lower().startswith("begin"):
323378
inside_statements_group = True
324379

325-
# semicolons inside the line? use them to separate statements
326-
# ... but not if they are inside a begin/end block (aka. statements group)
327-
sqlFragments = line.split(";")
328-
# no semicolons
329-
if len(sqlFragments) == 1:
330-
current_statement += line + " "
331-
continue
332-
quotes = 0
333-
# "select * from person;" -> ["select..", ""]
334-
for sqlFragment in sqlFragments:
335-
if len(sqlFragment.strip()) == 0:
336-
continue
337-
338-
if inside_statements_group:
339-
# if statements inside a begin/end block have semicolns,
340-
# they must persist - even with oracle
341-
sqlFragment += "; "
342-
343-
if proc_end_pattern.match(sqlFragment.lower()):
344-
inside_statements_group = False
345-
elif proc_start_pattern.match(sqlFragment.lower()):
346-
inside_statements_group = True
347-
elif sqlFragment.lower().startswith("begin"):
348-
inside_statements_group = True
349-
350-
# check if the semicolon is a part of the value (quoted string)
351-
quotes += sqlFragment.count("'")
352-
quotes -= sqlFragment.count("\\'")
353-
inside_quoted_string = quotes % 2 != 0
354-
if inside_quoted_string:
355-
sqlFragment += ";" # restore the semicolon
356-
357-
current_statement += sqlFragment
358-
if not inside_statements_group and not inside_quoted_string:
359-
statements_to_execute.append(current_statement.strip())
360-
current_statement = ""
361-
quotes = 0
362-
363-
current_statement = current_statement.strip()
364-
if len(current_statement) != 0:
365-
statements_to_execute.append(current_statement)
366-
367-
for statement in statements_to_execute:
368-
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
369-
omit_semicolon = not line_ends_with_proc_end.search(statement.lower())
370-
self._execute_sql(cur, statement, omit_semicolon)
371-
self._commit_if_needed(db_connection, no_transaction)
372-
except Exception as e:
373-
self._rollback_and_raise(db_connection, no_transaction, e)
380+
# check if the semicolon is a part of the value (quoted string)
381+
quotes += sqlFragment.count("'")
382+
quotes -= sqlFragment.count("\\'")
383+
inside_quoted_string = quotes % 2 != 0
384+
if inside_quoted_string:
385+
sqlFragment += ";" # restore the semicolon
386+
387+
current_statement += sqlFragment
388+
if not inside_statements_group and not inside_quoted_string:
389+
statements_to_execute.append(current_statement.strip())
390+
current_statement = ""
391+
quotes = 0
392+
393+
current_statement = current_statement.strip()
394+
if len(current_statement) != 0:
395+
statements_to_execute.append(current_statement)
396+
397+
return statements_to_execute
374398

375399
@renamed_args(
376400
mapping={
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SELECT * FROM person;
2+
SELECT * FROM person WHERE id=1;

test/tests/common_tests/script_files.robot

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Suite Teardown Disconnect From Database
66
Test Setup Create Person Table
77
Test Teardown Drop Tables Person And Foobar
88

9+
*** Variables ***
10+
${Script files dir} ${CURDIR}/../../resources/script_file_tests
11+
912

1013
*** Test Cases ***
1114
Semicolons As Statement Separators In One Line
@@ -35,9 +38,30 @@ Semicolons And Quotes In Values
3538
Should Be Equal As Strings ${results}[0] (5, 'Miles', "O'Brian")
3639
Should Be Equal As Strings ${results}[1] (6, 'Keiko', "O'Brian")
3740

41+
Split Script Into Statements - Internal Parser
42+
Insert Data In Person Table Using SQL Script
43+
@{Expected commands}= Create List
44+
... SELECT * FROM person
45+
... SELECT * FROM person WHERE id=1
46+
${extracted commands}= Split Sql Script ${Script files dir}/split_commands.sql
47+
Lists Should Be Equal ${Expected commands} ${extracted commands}
48+
FOR ${command} IN @{extracted commands}
49+
${results}= Query ${command}
50+
END
51+
52+
Split Script Into Statements - External Parser
53+
Insert Data In Person Table Using SQL Script
54+
@{Expected commands}= Create List
55+
... SELECT * FROM person;
56+
... SELECT * FROM person WHERE id=1;
57+
${extracted commands}= Split Sql Script ${Script files dir}/split_commands.sql external_parser=True
58+
Lists Should Be Equal ${Expected commands} ${extracted commands}
59+
FOR ${command} IN @{extracted commands}
60+
${results}= Query ${command}
61+
END
62+
3863

3964
*** Keywords ***
4065
Run SQL Script File
4166
[Arguments] ${File Name}
42-
${Script files dir}= Set Variable ${CURDIR}/../../resources/script_file_tests
4367
Execute Sql Script ${Script files dir}/${File Name}.sql

0 commit comments

Comments
 (0)