Skip to content

Commit 3d8a45e

Browse files
committed
Allow using sqlparse to split SQL script files (fix #205)
1 parent e38204b commit 3d8a45e

File tree

3 files changed

+75
-54
lines changed

3 files changed

+75
-54
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ readme = "README.md"
1515
requires-python = ">=3.8.1"
1616
dependencies = [
1717
"robotframework>=5.0.1",
18-
"robotframework-assertion-engine"
18+
"robotframework-assertion-engine",
19+
"sqlparse"
1920
]
2021
classifiers = [
2122
"Programming Language :: Python :: 3",

src/DatabaseLibrary/query.py

Lines changed: 62 additions & 52 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.
@@ -305,7 +309,7 @@ def execute_sql_script(
305309
omit_trailing_semicolon=db_connection.omit_trailing_semicolon,
306310
)
307311
else:
308-
statements_to_execute = self.split_sql_script(script_path)
312+
statements_to_execute = self.split_sql_script(script_path, external_parser=external_parser)
309313
for statement in statements_to_execute:
310314
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
311315
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
@@ -318,71 +322,77 @@ def execute_sql_script(
318322
def split_sql_script(
319323
self,
320324
script_path: str,
325+
external_parser=False,
321326
):
322327
"""
323328
Splits the content of the SQL script file loaded from `script_path` into individual SQL commands
324329
and returns them as a list of strings.
325330
SQL commands are expected to be delimited by a semicolon (';').
331+
332+
Set `external_parser` to _True_ to use the external `sqlparse` library.
326333
"""
327334
with open(script_path, encoding="UTF-8") as sql_file:
328335
logger.info("Splitting script file into statements...")
329336
statements_to_execute = []
330-
current_statement = ""
331-
inside_statements_group = False
332-
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
333-
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
334-
for line in sql_file:
335-
line = line.strip()
336-
if line.startswith("#") or line.startswith("--") or line == "/":
337-
continue
338-
339-
# check if the line matches the creating procedure regexp pattern
340-
if proc_start_pattern.match(line.lower()):
341-
inside_statements_group = True
342-
elif line.lower().startswith("begin"):
343-
inside_statements_group = True
344-
345-
# semicolons inside the line? use them to separate statements
346-
# ... but not if they are inside a begin/end block (aka. statements group)
347-
sqlFragments = line.split(";")
348-
# no semicolons
349-
if len(sqlFragments) == 1:
350-
current_statement += line + " "
351-
continue
352-
quotes = 0
353-
# "select * from person;" -> ["select..", ""]
354-
for sqlFragment in sqlFragments:
355-
if len(sqlFragment.strip()) == 0:
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 == "/":
356347
continue
357348

358-
if inside_statements_group:
359-
# if statements inside a begin/end block have semicolns,
360-
# they must persist - even with oracle
361-
sqlFragment += "; "
362-
363-
if proc_end_pattern.match(sqlFragment.lower()):
364-
inside_statements_group = False
365-
elif proc_start_pattern.match(sqlFragment.lower()):
349+
# check if the line matches the creating procedure regexp pattern
350+
if proc_start_pattern.match(line.lower()):
366351
inside_statements_group = True
367-
elif sqlFragment.lower().startswith("begin"):
352+
elif line.lower().startswith("begin"):
368353
inside_statements_group = True
369354

370-
# check if the semicolon is a part of the value (quoted string)
371-
quotes += sqlFragment.count("'")
372-
quotes -= sqlFragment.count("\\'")
373-
inside_quoted_string = quotes % 2 != 0
374-
if inside_quoted_string:
375-
sqlFragment += ";" # restore the semicolon
376-
377-
current_statement += sqlFragment
378-
if not inside_statements_group and not inside_quoted_string:
379-
statements_to_execute.append(current_statement.strip())
380-
current_statement = ""
381-
quotes = 0
382-
383-
current_statement = current_statement.strip()
384-
if len(current_statement) != 0:
385-
statements_to_execute.append(current_statement)
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:
366+
continue
367+
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()):
376+
inside_statements_group = True
377+
elif sqlFragment.lower().startswith("begin"):
378+
inside_statements_group = True
379+
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)
386396

387397
return statements_to_execute
388398

test/tests/common_tests/script_files.robot

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Semicolons And Quotes In Values
3838
Should Be Equal As Strings ${results}[0] (5, 'Miles', "O'Brian")
3939
Should Be Equal As Strings ${results}[1] (6, 'Keiko', "O'Brian")
4040

41-
Split Script Into Statements
41+
Split Script Into Statements - Internal Parser
4242
Insert Data In Person Table Using SQL Script
4343
@{Expected commands}= Create List
4444
... SELECT * FROM person
@@ -49,6 +49,16 @@ Split Script Into Statements
4949
${results}= Query ${command}
5050
END
5151

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
5262

5363

5464
*** Keywords ***

0 commit comments

Comments
 (0)