@@ -328,9 +328,7 @@ def execute_sql_script(
328328 else :
329329 statements_to_execute = self .split_sql_script (script_path , external_parser = external_parser )
330330 for statement in statements_to_execute :
331- proc_end_pattern = re .compile ("end(?!( if;| loop;| case;| while;| repeat;)).*;()?" )
332- line_ends_with_proc_end = re .compile (r"(\s|;)" + proc_end_pattern .pattern + "$" )
333- omit_semicolon = not line_ends_with_proc_end .search (statement .lower ())
331+ omit_semicolon = self ._omit_semicolon_needed (statement )
334332 self ._execute_sql (cur , statement , omit_semicolon , replace_robot_variables = replace_robot_variables )
335333 self ._commit_if_needed (db_connection , no_transaction )
336334 except Exception as e :
@@ -349,73 +347,83 @@ def split_sql_script(
349347 Set ``external_parser`` to _True_ to use the external library [https://pypi.org/project/sqlparse/|sqlparse].
350348 """
351349 with open (script_path , encoding = "UTF-8" ) as sql_file :
352- logger .info ("Splitting script file into statements..." )
353- statements_to_execute = []
354- if external_parser :
355- split_statements = sqlparse .split (sql_file .read ())
356- for statement in split_statements :
357- statement_without_comments = sqlparse .format (statement , strip_comments = True )
358- if statement_without_comments :
359- statements_to_execute .append (statement_without_comments )
360- else :
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 == "/" :
350+ return self .split_sql_string (sql_file .read (), external_parser = external_parser )
351+
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 (';').
357+
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 } " )
361+ statements_to_execute = []
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 == "/" :
376+ continue
377+
378+ # check if the line matches the creating procedure regexp pattern
379+ if proc_start_pattern .match (line .lower ()):
380+ inside_statements_group = True
381+ elif line .lower ().startswith ("begin" ):
382+ inside_statements_group = True
383+
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 :
368395 continue
369396
370- # check if the line matches the creating procedure regexp pattern
371- if proc_start_pattern .match (line .lower ()):
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 ()):
372405 inside_statements_group = True
373- elif line .lower ().startswith ("begin" ):
406+ elif sqlFragment .lower ().startswith ("begin" ):
374407 inside_statements_group = True
375408
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 :
387- continue
388-
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 ()):
397- inside_statements_group = True
398- elif sqlFragment .lower ().startswith ("begin" ):
399- inside_statements_group = True
400-
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-
418- return statements_to_execute
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 )
425+
426+ return statements_to_execute
419427
420428 @renamed_args (
421429 mapping = {
@@ -433,12 +441,20 @@ def execute_sql_string(
433441 omit_trailing_semicolon : Optional [bool ] = None ,
434442 * ,
435443 replace_robot_variables = False ,
444+ split : bool = False ,
445+ external_parser : bool = False ,
436446 sqlString : Optional [str ] = None ,
437447 sansTran : Optional [bool ] = None ,
438448 omitTrailingSemicolon : Optional [bool ] = None ,
439449 ):
440450 """
441- 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.
442458
443459 Set ``no_transaction`` to _True_ to run command without explicit transaction commit
444460 or rollback in case of error.
@@ -473,13 +489,20 @@ def execute_sql_string(
473489 cur = db_connection .client .cursor ()
474490 if omit_trailing_semicolon is None :
475491 omit_trailing_semicolon = db_connection .omit_trailing_semicolon
476- self ._execute_sql (
477- cur ,
478- sql_string ,
479- omit_trailing_semicolon = omit_trailing_semicolon ,
480- parameters = parameters ,
481- replace_robot_variables = replace_robot_variables ,
482- )
492+ if not split :
493+ self ._execute_sql (
494+ cur ,
495+ sql_string ,
496+ omit_trailing_semicolon = omit_trailing_semicolon ,
497+ parameters = parameters ,
498+ replace_robot_variables = replace_robot_variables ,
499+ )
500+ else :
501+ statements_to_execute = self .split_sql_string (sql_string , external_parser = external_parser )
502+ for statement in statements_to_execute :
503+ omit_semicolon = self ._omit_semicolon_needed (statement )
504+ self ._execute_sql (cur , statement , omit_semicolon , replace_robot_variables = replace_robot_variables )
505+
483506 self ._commit_if_needed (db_connection , no_transaction )
484507 except Exception as e :
485508 self ._rollback_and_raise (db_connection , no_transaction , e )
@@ -808,6 +831,17 @@ def set_logging_query_results(self, enabled: Optional[bool] = None, log_head: Op
808831 raise ValueError (f"Wrong log head value provided: { log_head } . The value can't be negative!" )
809832 self .LOG_QUERY_RESULTS_HEAD = log_head
810833
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+
811845 def _execute_sql (
812846 self ,
813847 cur ,
0 commit comments