1616import inspect
1717import re
1818import sys
19- from typing import List , Optional , Tuple , Union
19+ from typing import List , Optional , Tuple
2020
2121import sqlparse
2222from 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