"""Step definitions for sql_string_aware_coverage.feature. These steps target specific uncovered lines in _sql_string_aware.py: - Line 40: escaped single-quote handling (j += 2) in strip_sql_comments - Lines 52-58: terminated dollar-quoted strings in strip_sql_comments - Lines 60-62: unterminated dollar-quoted strings in strip_sql_comments - Lines 70-71: nested block comment depth increment in strip_sql_comments - Line 115: escaped single-quote handling (i += 2) in find_unquoted_semicolon - Lines 125-130: dollar-quoted string handling in find_unquoted_semicolon """ from behave import given, then, when from cleveragents.domain.models.acms._sql_string_aware import ( find_unquoted_semicolon, strip_sql_comments, ) # --------------------------------------------------------------------------- # Background # --------------------------------------------------------------------------- @given("the sql_string_aware module is imported") def step_module_imported(context): """Ensure the module is importable.""" assert strip_sql_comments is not None assert find_unquoted_semicolon is not None # --------------------------------------------------------------------------- # Generic helpers for setting SQL content and calling functions # --------------------------------------------------------------------------- @given('SQL content "{sql}"') def step_set_sql_content(context, sql): """Store literal SQL content on the context.""" context.sql_content = sql @when("I strip SQL comments") def step_strip_comments(context): """Call strip_sql_comments on the stored content.""" context.stripped_result = strip_sql_comments(context.sql_content) @then('the stripped result should be "{expected}"') def step_verify_stripped(context, expected): """Assert the stripped result matches the expected string.""" assert context.stripped_result == expected, ( f"Expected {expected!r}, got {context.stripped_result!r}" ) @when("I search for the first unquoted semicolon") def step_find_semicolon(context): """Call find_unquoted_semicolon on the stored content.""" context.semicolon_index = find_unquoted_semicolon(context.sql_content) # --------------------------------------------------------------------------- # strip_sql_comments — terminated anonymous dollar-quoted string (lines 52-58) # --------------------------------------------------------------------------- @given("SQL content with a terminated anonymous dollar-quoted string") def step_sql_anon_dollar_string(context): """Create SQL with $$body$$ — anonymous dollar-quoted string.""" context.sql_content = "SELECT $$hello world$$" @then("the dollar-quoted body should be preserved intact") def step_verify_anon_dollar_preserved(context): """Verify the anonymous dollar-quoted string is preserved.""" assert context.stripped_result == "SELECT $$hello world$$", ( f"Got {context.stripped_result!r}" ) # --------------------------------------------------------------------------- # strip_sql_comments — terminated named dollar-quoted string (lines 52-58) # --------------------------------------------------------------------------- @given("SQL content with a terminated named dollar-quoted string") def step_sql_named_dollar_string(context): """Create SQL with $tag$body$tag$ — named dollar-quoted string.""" context.sql_content = "SELECT $fn$CREATE FUNCTION$fn$ AS val" @then("the named dollar-quoted body should be preserved intact") def step_verify_named_dollar_preserved(context): """Verify the named dollar-quoted string is preserved.""" assert context.stripped_result == "SELECT $fn$CREATE FUNCTION$fn$ AS val", ( f"Got {context.stripped_result!r}" ) # --------------------------------------------------------------------------- # strip_sql_comments — unterminated dollar-quoted string (lines 60-62) # --------------------------------------------------------------------------- @given("SQL content with an unterminated dollar-quoted string") def step_sql_unterminated_dollar_string(context): """Create SQL with a $$ that never closes.""" context.sql_content = "SELECT $$this never ends" @then("the rest of the content should be preserved as-is") def step_verify_unterminated_dollar_preserved(context): """The entire remainder from $$ onwards should be kept.""" assert context.stripped_result == "SELECT $$this never ends", ( f"Got {context.stripped_result!r}" ) # --------------------------------------------------------------------------- # find_unquoted_semicolon — escaped single quotes (line 115) # --------------------------------------------------------------------------- @then("the semicolon index should point to the one after the closing quote") def step_verify_semicolon_after_quote(context): """The semicolon at the position right after the closing quote. Content: SELECT 'it''s;here'; DROP TABLE Indices: 0123456789... The string 'it''s;here' starts at index 7 and ends at index 19 (the closing ') The unquoted ; is at index 20. """ sql = context.sql_content # The unquoted semicolon should be at the position of ';' after the closing quote idx = context.semicolon_index assert idx == sql.index(";", sql.rindex("'") + 1), ( f"Expected semicolon after closing quote, got index {idx}" ) @then("the semicolon index should be {index:d}") def step_verify_semicolon_index(context, index): """Assert the semicolon index matches the expected value.""" assert context.semicolon_index == index, ( f"Expected {index}, got {context.semicolon_index}" ) # --------------------------------------------------------------------------- # find_unquoted_semicolon — terminated dollar-quoted strings (lines 125-129) # --------------------------------------------------------------------------- @given( "SQL content with dollar-quoted string containing a semicolon then real semicolon" ) def step_sql_dollar_with_inner_semi(context): """Content: $$a;b$$;X — semicolon inside $$ should be skipped.""" context.sql_content = "$$a;b$$;X" @then("the semicolon should be found after the dollar-quoted string") def step_verify_semi_after_dollar(context): """The unquoted semicolon is at index 7 (right after $$a;b$$).""" assert context.semicolon_index == 7, f"Expected 7, got {context.semicolon_index}" @given( "SQL content with named dollar-quoted string containing a semicolon then real semicolon" ) def step_sql_named_dollar_with_inner_semi(context): """Content: $t$a;b$t$;X — semicolon inside $t$ should be skipped.""" context.sql_content = "$t$a;b$t$;X" @then("the semicolon should be found after the named dollar-quoted string") def step_verify_semi_after_named_dollar(context): """The unquoted semicolon is at index 9 (right after $t$a;b$t$).""" assert context.semicolon_index == 9, f"Expected 9, got {context.semicolon_index}" # --------------------------------------------------------------------------- # find_unquoted_semicolon — unterminated dollar-quoted (line 129 else branch) # --------------------------------------------------------------------------- @given("SQL content with an unterminated dollar-quoted string containing a semicolon") def step_sql_unterminated_dollar_with_semi(context): """Content: $$a;b — the $$ never closes, so the ; inside is quoted.""" context.sql_content = "$$a;b" @then("ssacov the result should be -1 indicating no unquoted semicolon") def step_verify_no_semicolon(context): """No unquoted semicolon exists.""" assert context.semicolon_index == -1, f"Expected -1, got {context.semicolon_index}" # --------------------------------------------------------------------------- # strip_sql_comments — mix of dollar-quoted strings and comments # --------------------------------------------------------------------------- @given("SQL content mixing dollar-quoted strings and comments") def step_sql_mixed(context): """Dollar-quoted string followed by a line comment and more SQL.""" context.sql_content = "$$body$$ -- this is a comment\nSELECT 1" @then("only the comments should be removed and dollar strings preserved") def step_verify_mixed(context): """The dollar-quoted string stays, the comment is removed.""" assert context.stripped_result == "$$body$$ \nSELECT 1", ( f"Got {context.stripped_result!r}" ) # --------------------------------------------------------------------------- # find_unquoted_semicolon with start offset # --------------------------------------------------------------------------- @when("I search for unquoted semicolon starting at offset {offset:d}") def step_find_semicolon_with_offset(context, offset): """Call find_unquoted_semicolon with a start offset.""" context.semicolon_index = find_unquoted_semicolon(context.sql_content, offset) @then("the semicolon should be found at index {index:d}") def step_verify_semicolon_at_index(context, index): """Assert the semicolon was found at the expected index.""" assert context.semicolon_index == index, ( f"Expected {index}, got {context.semicolon_index}" )