1818import sys
1919from typing import List , Optional , Tuple
2020
21+ import sqlparse
2122from robot .api import logger
2223from 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 = {
0 commit comments