1313# limitations under the License.
1414
1515import inspect
16+ import re
1617import sys
1718from typing import List , Optional
1819
@@ -208,11 +209,17 @@ def delete_all_rows_from_table(self, tableName: str, sansTran: bool = False, ali
208209 if cur and not sansTran :
209210 db_connection .client .rollback ()
210211
211- def execute_sql_script (self , sqlScriptFileName : str , sansTran : bool = False , alias : Optional [str ] = None ):
212+ def execute_sql_script (
213+ self , sqlScriptFileName : str , sansTran : bool = False , split : bool = True , alias : Optional [str ] = None
214+ ):
212215 """
213216 Executes the content of the `sqlScriptFileName` as SQL commands. Useful for setting the database to a known
214217 state before running your tests, or clearing out your test data after running each a test.
215218
219+ SQL commands are expected to be delimited by a semicolon (';') - they will be split and executed separately.
220+ You can disable this behaviour setting the parameter `split` to _False_ -
221+ in this case the entire script content will be passed to the database module for execution.
222+
216223 Sample usage :
217224 | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-setup.sql |
218225 | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-setup.sql |
@@ -221,7 +228,6 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali
221228 | Execute Sql Script | ${EXECDIR}${/}resources${/}DML-teardown.sql |
222229 | Execute Sql Script | ${EXECDIR}${/}resources${/}DDL-teardown.sql |
223230
224- SQL commands are expected to be delimited by a semicolon (';') - they will be executed separately.
225231
226232 For example:
227233 DELETE FROM person_employee_table;
@@ -272,76 +278,101 @@ def execute_sql_script(self, sqlScriptFileName: str, sansTran: bool = False, ali
272278 with open (sqlScriptFileName , encoding = "UTF-8" ) as sql_file :
273279 cur = None
274280 try :
275- statements_to_execute = []
276281 cur = db_connection .client .cursor ()
277282 logger .info (f"Executing : Execute SQL Script | { sqlScriptFileName } " )
278- current_statement = ""
279- inside_statements_group = False
280-
281- for line in sql_file :
282- line = line .strip ()
283- if line .startswith ("#" ) or line .startswith ("--" ) or line == "/" :
284- continue
285- if line .lower ().startswith ("begin" ):
286- inside_statements_group = True
287-
288- # semicolons inside the line? use them to separate statements
289- # ... but not if they are inside a begin/end block (aka. statements group)
290- sqlFragments = line .split (";" )
291- # no semicolons
292- if len (sqlFragments ) == 1 :
293- current_statement += line + " "
294- continue
295- quotes = 0
296- # "select * from person;" -> ["select..", ""]
297- for sqlFragment in sqlFragments :
298- if len (sqlFragment .strip ()) == 0 :
283+ if not split :
284+ logger .info ("Statements splitting disabled - pass entire script content to the database module" )
285+ self .__execute_sql (cur , sql_file .read ())
286+ else :
287+ logger .info ("Splitting script file into statements..." )
288+ statements_to_execute = []
289+ current_statement = ""
290+ inside_statements_group = False
291+ proc_start_pattern = re .compile ("create( or replace)? (procedure|function){1}( )?" )
292+ proc_end_pattern = re .compile ("end(?!( if;| loop;| case;| while;| repeat;)).*;()?" )
293+ for line in sql_file :
294+ line = line .strip ()
295+ if line .startswith ("#" ) or line .startswith ("--" ) or line == "/" :
299296 continue
300- if inside_statements_group :
301- # if statements inside a begin/end block have semicolns,
302- # they must persist - even with oracle
303- sqlFragment += "; "
304- if sqlFragment .lower () == "end; " :
305- inside_statements_group = False
306- elif sqlFragment .lower ().startswith ("begin" ):
297+
298+ # check if the line matches the creating procedure regexp pattern
299+ if proc_start_pattern .match (line .lower ()):
300+ inside_statements_group = True
301+ elif line .lower ().startswith ("begin" ):
307302 inside_statements_group = True
308303
309- # check if the semicolon is a part of the value (quoted string)
310- quotes += sqlFragment .count ("'" )
311- quotes -= sqlFragment .count ("\\ '" )
312- quotes -= sqlFragment .count ("''" )
313- inside_quoted_string = quotes % 2 != 0
314- if inside_quoted_string :
315- sqlFragment += ";" # restore the semicolon
316-
317- current_statement += sqlFragment
318- if not inside_statements_group and not inside_quoted_string :
319- statements_to_execute .append (current_statement .strip ())
320- current_statement = ""
321- quotes = 0
322-
323- current_statement = current_statement .strip ()
324- if len (current_statement ) != 0 :
325- statements_to_execute .append (current_statement )
326-
327- for statement in statements_to_execute :
328- logger .info (f"Executing statement from script file: { statement } " )
329- omit_semicolon = not statement .lower ().endswith ("end;" )
330- self .__execute_sql (cur , statement , omit_semicolon )
304+ # semicolons inside the line? use them to separate statements
305+ # ... but not if they are inside a begin/end block (aka. statements group)
306+ sqlFragments = line .split (";" )
307+ # no semicolons
308+ if len (sqlFragments ) == 1 :
309+ current_statement += line + " "
310+ continue
311+ quotes = 0
312+ # "select * from person;" -> ["select..", ""]
313+ for sqlFragment in sqlFragments :
314+ if len (sqlFragment .strip ()) == 0 :
315+ continue
316+
317+ if inside_statements_group :
318+ # if statements inside a begin/end block have semicolns,
319+ # they must persist - even with oracle
320+ sqlFragment += "; "
321+
322+ if proc_end_pattern .match (sqlFragment .lower ()):
323+ inside_statements_group = False
324+ elif proc_start_pattern .match (sqlFragment .lower ()):
325+ inside_statements_group = True
326+ elif sqlFragment .lower ().startswith ("begin" ):
327+ inside_statements_group = True
328+
329+ # check if the semicolon is a part of the value (quoted string)
330+ quotes += sqlFragment .count ("'" )
331+ quotes -= sqlFragment .count ("\\ '" )
332+ quotes -= sqlFragment .count ("''" )
333+ inside_quoted_string = quotes % 2 != 0
334+ if inside_quoted_string :
335+ sqlFragment += ";" # restore the semicolon
336+
337+ current_statement += sqlFragment
338+ if not inside_statements_group and not inside_quoted_string :
339+ statements_to_execute .append (current_statement .strip ())
340+ current_statement = ""
341+ quotes = 0
342+
343+ current_statement = current_statement .strip ()
344+ if len (current_statement ) != 0 :
345+ statements_to_execute .append (current_statement )
346+
347+ for statement in statements_to_execute :
348+ logger .info (f"Executing statement from script file: { statement } " )
349+ line_ends_with_proc_end = re .compile (r"(\s|;)" + proc_end_pattern .pattern + "$" )
350+ omit_semicolon = not line_ends_with_proc_end .search (statement .lower ())
351+ self .__execute_sql (cur , statement , omit_semicolon )
331352 if not sansTran :
332353 db_connection .client .commit ()
333354 finally :
334355 if cur and not sansTran :
335356 db_connection .client .rollback ()
336357
337358 def execute_sql_string (
338- self , sqlString : str , sansTran : bool = False , alias : Optional [str ] = None , parameters : Optional [List ] = None
359+ self ,
360+ sqlString : str ,
361+ sansTran : bool = False ,
362+ omitTrailingSemicolon : Optional [bool ] = None ,
363+ alias : Optional [str ] = None ,
364+ parameters : Optional [List ] = None ,
339365 ):
340366 """
341- Executes the sqlString as SQL commands. Useful to pass arguments to your sql.
342- SQL commands are expected to be delimited by a semicolon (';').
367+ Executes the ``sqlString`` as a single SQL command.
343368
344- Use optional `sansTran` to run command without an explicit transaction commit or rollback:
369+ Use optional ``sansTran`` to run command without an explicit transaction commit or rollback.
370+
371+ Use optional ``omitTrailingSemicolon`` parameter for explicit instruction,
372+ if the trailing semicolon (;) at the SQL string end should be removed or not:
373+ - Some database modules (e.g. Oracle) throw an exception, if you leave a semicolon at the string end
374+ - However, there are exceptional cases, when you need it even for Oracle - e.g. at the end of a PL/SQL block.
375+ - If not specified, it's decided based on the current database module in use. For Oracle, the semicolon is removed by default.
345376
346377 Use optional ``alias`` parameter to specify what connection should be used for the query if you have more
347378 than one connection open.
@@ -353,6 +384,7 @@ def execute_sql_string(
353384 | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table |
354385 | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | alias=my_alias |
355386 | Execute Sql String | DELETE FROM person_employee_table; DELETE FROM person_table | sansTran=True |
387+ | Execute Sql String | CREATE PROCEDURE proc AS BEGIN DBMS_OUTPUT.PUT_LINE('Hello!'); END; | omitTrailingSemicolon=False |
356388 | @{parameters} | Create List | person_employee_table |
357389 | Execute Sql String | SELECT * FROM %s | parameters=${parameters} |
358390 """
@@ -361,7 +393,7 @@ def execute_sql_string(
361393 try :
362394 cur = db_connection .client .cursor ()
363395 logger .info (f"Executing : Execute SQL String | { sqlString } " )
364- self .__execute_sql (cur , sqlString , parameters = parameters )
396+ self .__execute_sql (cur , sqlString , omit_trailing_semicolon = omitTrailingSemicolon , parameters = parameters )
365397 if not sansTran :
366398 db_connection .client .commit ()
367399 finally :
0 commit comments