Skip to content

feat(rules): add W016 not-in-with-subquery rule#26

Merged
Pawansingh3889 merged 1 commit intoPawansingh3889:mainfrom
mvanhorn:osc/6-w016-not-in-with-subquery
Apr 25, 2026
Merged

feat(rules): add W016 not-in-with-subquery rule#26
Pawansingh3889 merged 1 commit intoPawansingh3889:mainfrom
mvanhorn:osc/6-w016-not-in-with-subquery

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

What changed

Adds a new warning rule W016 not-in-with-subquery that catches WHERE col NOT IN (SELECT ...). When the subquery returns any NULL, the predicate evaluates to UNKNOWN for every outer row and the query silently returns zero results. This is the bug described in issue #6 (the regex and acceptance examples come straight from the issue body).

Related issue

Closes #6

If this adds or changes a rule

  • Rule ID is the next unused one in its family — W016 (not used elsewhere; verified via grep -rn '"W016"' sql_guard/rules)
  • Rule class registered in sql_guard/rules/__init__.py (NotInWithSubquery added to imports + ALL_RULES, comment updated to W001-W012, W016)
  • Fixture line in tests/fixtures/warnings.sql (the canonical customers NOT IN (SELECT customer_id FROM orders) example from the issue)
  • "Fires on bad SQL" test in tests/test_rules.py (test_w016_not_in_with_subquery runs against the fixture)
  • "Does NOT fire on safe SQL" test using tmp_path (test_w016_not_in_value_list_ok confirms NOT IN (1, 2, 3) value lists are not flagged)
  • README rule table updated (added W016 row to the warnings table after W010). Key Numbers block left as-is since it's already a release behind (says 24 rules / 14 warnings / version 0.4.1 while v0.5.0 is on main with 25 rules); fixing that drift seemed out of scope here.
  • Not a removal or rename

Tests

  • python3 -m pytest tests/test_rules.py -q -> 32 passed in 0.02s (the two new W016 tests included)
  • ruff check sql_guard/rules/warnings.py sql_guard/rules/__init__.py tests/test_rules.py -> All checks passed!

The full pytest tests/ run shows one pre-existing failure in tests/test_structural.py::TestDeeplyNestedSubquery::test_deep_nesting_detected that reproduces on upstream/main (a507f00) before this branch — confirmed by checking out main and running the single test in isolation. Not introduced by this change.

Checklist

  • One logical change per commit, conventional commit style (feat(rules): ...)
  • CHANGELOG.md [Unreleased] entry added under ### Added

Anything else reviewers should know

The rule uses multiline = True + check_statement because the issue's footgun pattern naturally spans lines (a real-world NOT IN (SELECT ...) typically has the subquery on its own line). The chosen regex matches the issue's r"\bNOT\s+IN\s*\(\s*SELECT\b" exactly, which means it won't trigger on value lists like NOT IN (1, 2, 3) — that case is asserted by test_w016_not_in_value_list_ok.

This contribution was developed with AI assistance (Codex).

Closes Pawansingh3889#6

NOT IN (SELECT ...) returns zero rows if the subquery contains NULL,
which is a common SQL footgun. The rule warns and suggests NOT EXISTS
or LEFT JOIN ... IS NULL as alternatives.

- Add NotInWithSubquery to sql_guard/rules/warnings.py
- Register W016 in ALL_RULES
- Add fixture entry to tests/fixtures/warnings.sql
- Update rule counts (25 -> 26, 17 -> 18 warnings)
- Cover both the trigger case and value-list-ok case in tests
@Pawansingh3889
Copy link
Copy Markdown
Owner

Thanks for this, really clean PR. Template is filled out properly, the tests cover both directions, and you spotted the pre-existing structural test failure on main, which saves me time tracking it down separately.

One thing I appreciated: the regex \bNOT\s+IN\s*\(\s*SELECT\b is exactly the right narrowness, catches the SELECT subquery footgun without false-positiving on value lists. The multiline flag plus the negative test for NOT IN (1, 2, 3) make that intent clear in the test file too.

You were right to leave the README Key Numbers drift alone. That's mine to fix in the next release commit, not yours to clean up here. Same with the structural test failure, I'll open a separate issue for it.

Squash-merging now. Thanks again for taking the time on this one.

@Pawansingh3889 Pawansingh3889 merged commit 525ddea into Pawansingh3889:main Apr 25, 2026
13 checks passed
@mvanhorn
Copy link
Copy Markdown
Contributor Author

Thanks for the merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

rule: W016 not-in-with-subquery - warn on NOT IN (SELECT ...)

2 participants