Skip to content

feat(rules): add W011 union-without-all warning rule#12

Merged
Pawansingh3889 merged 1 commit intoPawansingh3889:mainfrom
tmchow:osc/1-w011-union-without-all
Apr 19, 2026
Merged

feat(rules): add W011 union-without-all warning rule#12
Pawansingh3889 merged 1 commit intoPawansingh3889:mainfrom
tmchow:osc/1-w011-union-without-all

Conversation

@tmchow
Copy link
Copy Markdown
Contributor

@tmchow tmchow commented Apr 19, 2026

Summary

Implements W011 union-without-all per #1. The rule warns when UNION is used without ALL, since bare UNION forces a sort-and-dedupe pass that is often unnecessary when the two sides are disjoint (different date-range tables, different customer IDs, etc.).

Why this matters

The issue body flags a real cost: UNION forces a sort-and-dedupe pass while UNION ALL concatenates. On large result sets the difference is often an order of magnitude. W011 is an advisory warning so the author has to actively justify the dedupe cost.

Mirrors the single-statement multiline shape of the existing SubqueryCouldBeJoin (W005) rule that the issue cited as the template. Regex �UNION�(?!\s+ALL�) matches bare UNION but not UNION ALL.

Changes

  • sql_guard/rules/warnings.py: new UnionWithoutAll class (W011). Copies the shape of SubqueryCouldBeJoin / OrderByWithoutLimit - multiline statement check, Rule._compile(...) pattern, standard Finding return.
  • sql_guard/rules/__init__.py: import UnionWithoutAll, register it in ALL_RULES after CommentedOutCode, and bump the # Warnings (W001-W010) section comment to W001-W011.
  • tests/fixtures/warnings.sql: add a triggering case (UNION between two orders_* selects).
  • tests/test_rules.py:
    • test_all_rules_loaded: 19 -> 20
    • test_13_warnings -> test_14_warnings, 13 -> 14 (W-series goes 10 -> 11, plus the 3 structural)
    • test_w011_union_without_all: asserts a W011 finding fires on the fixture
    • test_w011_passes_on_union_all: constructs UnionWithoutAll() directly and asserts UNION ALL is NOT flagged (regression test for the lookahead)

Testing

$ python -m pytest tests/test_rules.py -q
24 passed, 1 pre-existing duration_tracked failure unrelated to this change

The pre-existing TestChecker.test_duration_tracked failure (assert 0.0 > 0) is on main too; fast machines complete the check faster than the resolution of time.perf_counter captures. Not touched here.

Fixes #1


This contribution was developed with AI assistance (Claude Code).

Compound Engineering

Implements the `W011 union-without-all` rule requested in Pawansingh3889#1.
`UNION` forces a sort-and-dedupe pass; `UNION ALL` concatenates. The rule
warns when `UNION` is used without `ALL`, letting the author actively
decide whether duplicate-removal is actually wanted.

Mirrors the single-statement multiline shape of the existing
`SubqueryCouldBeJoin` (W005) rule. Regex `\bUNION\b(?!\s+ALL\b)` matches
bare `UNION` but not `UNION ALL`. Added to `ALL_RULES`; registry-count
assertions updated from 19 -> 20 and W-series warnings from 13 -> 14.

Tests:
- `tests/fixtures/warnings.sql` gets a triggering case
  (`UNION` between two `orders_*` selects)
- `test_w011_union_without_all` checks that a W011 finding is reported
- `test_w011_passes_on_union_all` constructs the rule directly and
  verifies `UNION ALL` is NOT flagged

Fixes Pawansingh3889#1
@tmchow tmchow requested a review from Pawansingh3889 as a code owner April 19, 2026 07:51
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your first PR on sql-sop! Quick checks:

  • pytest -q and ruff check . pass locally
  • Every new rule has both a "fires on bad SQL" test AND a
    "does NOT fire on safe SQL" test
  • Rule IDs (E001, W001, …) are public API — don't renumber
    or rename existing ones without the deprecation process in
    GOVERNANCE.md
  • CHANGELOG.md [Unreleased] entry for any user-facing change

First-PR-wins on the linked issue applies for 7 days. The
maintainer will review within 14 days of your last response.

@Pawansingh3889 Pawansingh3889 changed the title W011: union-without-all warning rule feat(rules): add W011 union-without-all warning rule Apr 19, 2026
Copy link
Copy Markdown
Owner

@Pawansingh3889 Pawansingh3889 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Nice first PR, thanks.

Matches the W005 template, both tests in place (the UNION ALL regression is a good call), 80 passing locally ✅

Renamed the title so the conventional-commits check passes - that's what validate was failing on. The test_duration_tracked thing you flagged is a known flaky test, not from this pr.

Merging, closes #1. #2-10 are in the same shape if you fancy another 🙌

@Pawansingh3889 Pawansingh3889 merged commit d24b2d2 into Pawansingh3889:main Apr 19, 2026
2 of 3 checks passed
Pawansingh3889 added a commit that referenced this pull request Apr 27, 2026
Trevin Chow shipped two rules pre-v0.6 that were missed when the
Contributors section first landed:

- W011 union-without-all (PR #12)
- P005 sqlalchemy-text-fstring (PR #25), one of the five Python rules

Reordered the table chronologically by first contribution so the
thank-you reads in joining order: tmchow, mvanhorn, Prabhu-1409.
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: W011 union-without-all - warn on UNION when UNION ALL would work

2 participants