Skip to content

Commit 64de753

Browse files
committed
Finalize the structure, make minor fixes, add docs
1 parent e203372 commit 64de753

File tree

1 file changed

+87
-76
lines changed

1 file changed

+87
-76
lines changed

src/DatabaseLibrary/query.py

Lines changed: 87 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import inspect
1717
import re
1818
import sys
19-
from typing import List, Optional, Tuple, Union
19+
from typing import List, Optional, Tuple
2020

2121
import sqlparse
2222
from robot.api import logger
@@ -347,82 +347,82 @@ def split_sql_script(
347347
Set ``external_parser`` to _True_ to use the external library [https://pypi.org/project/sqlparse/|sqlparse].
348348
"""
349349
with open(script_path, encoding="UTF-8") as sql_file:
350-
logger.info("Splitting script file into statements...")
351350
return self.split_sql_string(sql_file.read(), external_parser=external_parser)
352351

353-
def split_sql_string(self, sql_string: str, external_parser: bool = False):
354-
if external_parser:
355-
return self._split_statements_using_external_parser(sql_string)
356-
else:
357-
return self._parse_sql_internally(sql_string.splitlines())
352+
def split_sql_string(self, sql_string: str, external_parser=False):
353+
"""
354+
Splits the content of the ``sql_string`` into individual SQL commands
355+
and returns them as a list of strings.
356+
SQL commands are expected to be delimited by a semicolon (';').
358357
359-
def _parse_sql_internally(self, sql_file: List[str]) -> list[str]:
358+
Set ``external_parser`` to _True_ to use the external library [https://pypi.org/project/sqlparse/|sqlparse].
359+
"""
360+
logger.info(f"Splitting SQL into statements. Using external parser: {external_parser}")
360361
statements_to_execute = []
361-
current_statement = ""
362-
inside_statements_group = False
363-
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
364-
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
365-
for line in sql_file:
366-
line = line.strip()
367-
if line.startswith("#") or line.startswith("--") or line == "/":
368-
continue
369-
370-
# check if the line matches the creating procedure regexp pattern
371-
if proc_start_pattern.match(line.lower()):
372-
inside_statements_group = True
373-
elif line.lower().startswith("begin"):
374-
inside_statements_group = True
375-
376-
# semicolons inside the line? use them to separate statements
377-
# ... but not if they are inside a begin/end block (aka. statements group)
378-
sqlFragments = line.split(";")
379-
# no semicolons
380-
if len(sqlFragments) == 1:
381-
current_statement += line + " "
382-
continue
383-
quotes = 0
384-
# "select * from person;" -> ["select..", ""]
385-
for sqlFragment in sqlFragments:
386-
if len(sqlFragment.strip()) == 0:
362+
if external_parser:
363+
split_statements = sqlparse.split(sql_string)
364+
for statement in split_statements:
365+
statement_without_comments = sqlparse.format(statement, strip_comments=True)
366+
if statement_without_comments:
367+
statements_to_execute.append(statement_without_comments)
368+
else:
369+
current_statement = ""
370+
inside_statements_group = False
371+
proc_start_pattern = re.compile("create( or replace)? (procedure|function){1}( )?")
372+
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
373+
for line in sql_string.splitlines():
374+
line = line.strip()
375+
if line.startswith("#") or line.startswith("--") or line == "/":
387376
continue
388377

389-
if inside_statements_group:
390-
# if statements inside a begin/end block have semicolns,
391-
# they must persist - even with oracle
392-
sqlFragment += "; "
393-
394-
if proc_end_pattern.match(sqlFragment.lower()):
395-
inside_statements_group = False
396-
elif proc_start_pattern.match(sqlFragment.lower()):
378+
# check if the line matches the creating procedure regexp pattern
379+
if proc_start_pattern.match(line.lower()):
397380
inside_statements_group = True
398-
elif sqlFragment.lower().startswith("begin"):
381+
elif line.lower().startswith("begin"):
399382
inside_statements_group = True
400383

401-
# check if the semicolon is a part of the value (quoted string)
402-
quotes += sqlFragment.count("'")
403-
quotes -= sqlFragment.count("\\'")
404-
inside_quoted_string = quotes % 2 != 0
405-
if inside_quoted_string:
406-
sqlFragment += ";" # restore the semicolon
407-
408-
current_statement += sqlFragment
409-
if not inside_statements_group and not inside_quoted_string:
410-
statements_to_execute.append(current_statement.strip())
411-
current_statement = ""
412-
quotes = 0
413-
414-
current_statement = current_statement.strip()
415-
if len(current_statement) != 0:
416-
statements_to_execute.append(current_statement)
417-
return statements_to_execute
384+
# semicolons inside the line? use them to separate statements
385+
# ... but not if they are inside a begin/end block (aka. statements group)
386+
sqlFragments = line.split(";")
387+
# no semicolons
388+
if len(sqlFragments) == 1:
389+
current_statement += line + " "
390+
continue
391+
quotes = 0
392+
# "select * from person;" -> ["select..", ""]
393+
for sqlFragment in sqlFragments:
394+
if len(sqlFragment.strip()) == 0:
395+
continue
396+
397+
if inside_statements_group:
398+
# if statements inside a begin/end block have semicolns,
399+
# they must persist - even with oracle
400+
sqlFragment += "; "
401+
402+
if proc_end_pattern.match(sqlFragment.lower()):
403+
inside_statements_group = False
404+
elif proc_start_pattern.match(sqlFragment.lower()):
405+
inside_statements_group = True
406+
elif sqlFragment.lower().startswith("begin"):
407+
inside_statements_group = True
408+
409+
# check if the semicolon is a part of the value (quoted string)
410+
quotes += sqlFragment.count("'")
411+
quotes -= sqlFragment.count("\\'")
412+
inside_quoted_string = quotes % 2 != 0
413+
if inside_quoted_string:
414+
sqlFragment += ";" # restore the semicolon
415+
416+
current_statement += sqlFragment
417+
if not inside_statements_group and not inside_quoted_string:
418+
statements_to_execute.append(current_statement.strip())
419+
current_statement = ""
420+
quotes = 0
421+
422+
current_statement = current_statement.strip()
423+
if len(current_statement) != 0:
424+
statements_to_execute.append(current_statement)
418425

419-
def _split_statements_using_external_parser(self, sql_file_content: str):
420-
statements_to_execute = []
421-
split_statements = sqlparse.split(sql_file_content)
422-
for statement in split_statements:
423-
statement_without_comments = sqlparse.format(statement, strip_comments=True)
424-
if statement_without_comments:
425-
statements_to_execute.append(statement_without_comments)
426426
return statements_to_execute
427427

428428
@renamed_args(
@@ -441,14 +441,20 @@ def execute_sql_string(
441441
omit_trailing_semicolon: Optional[bool] = None,
442442
*,
443443
replace_robot_variables=False,
444+
split: bool = False,
445+
external_parser: bool = False,
444446
sqlString: Optional[str] = None,
445447
sansTran: Optional[bool] = None,
446448
omitTrailingSemicolon: Optional[bool] = None,
447-
split: bool = False,
448-
external_parser: bool = False,
449449
):
450450
"""
451-
Executes the ``sql_string`` as a single SQL command.
451+
Executes the ``sql_string`` - as a single SQL command (default) or as separate statements.
452+
453+
Set ``split`` to _True_ to enable dividing the string into SQL commands similar to the `Execute SQL Script`
454+
keyword. The commands are expected to be delimited by a semicolon (';') in this case -
455+
they will be split and executed separately.
456+
457+
Set ``external_parser`` to _True_ to use the external library [https://pypi.org/project/sqlparse/|sqlparse] for splitting the script.
452458
453459
Set ``no_transaction`` to _True_ to run command without explicit transaction commit
454460
or rollback in case of error.
@@ -501,12 +507,6 @@ def execute_sql_string(
501507
except Exception as e:
502508
self._rollback_and_raise(db_connection, no_transaction, e)
503509

504-
def _omit_semicolon_needed(self, statement: str) -> bool:
505-
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
506-
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
507-
omit_semicolon = not line_ends_with_proc_end.search(statement.lower())
508-
return omit_semicolon
509-
510510
@renamed_args(mapping={"spName": "procedure_name", "spParams": "procedure_params", "sansTran": "no_transaction"})
511511
def call_stored_procedure(
512512
self,
@@ -831,6 +831,17 @@ def set_logging_query_results(self, enabled: Optional[bool] = None, log_head: Op
831831
raise ValueError(f"Wrong log head value provided: {log_head}. The value can't be negative!")
832832
self.LOG_QUERY_RESULTS_HEAD = log_head
833833

834+
def _omit_semicolon_needed(self, statement: str) -> bool:
835+
"""
836+
Checks if the `statement` ends with a procedure ending keyword - so that semicolon should be omitted -
837+
and returns the result.
838+
The function is used when running multiple SQL statements from a script or an SQL string.
839+
"""
840+
proc_end_pattern = re.compile("end(?!( if;| loop;| case;| while;| repeat;)).*;()?")
841+
line_ends_with_proc_end = re.compile(r"(\s|;)" + proc_end_pattern.pattern + "$")
842+
omit_semicolon = not line_ends_with_proc_end.search(statement.lower())
843+
return omit_semicolon
844+
834845
def _execute_sql(
835846
self,
836847
cur,

0 commit comments

Comments
 (0)