jarify 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. {jarify-0.3.1 → jarify-0.3.2}/CHANGELOG.md +5 -0
  2. {jarify-0.3.1 → jarify-0.3.2}/PKG-INFO +1 -1
  3. {jarify-0.3.1 → jarify-0.3.2}/pyproject.toml +1 -1
  4. jarify-0.3.2/src/jarify/rules/prefer_group_by_all.py +119 -0
  5. jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.expected.sql +5 -0
  6. jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.input.sql +5 -0
  7. {jarify-0.3.1 → jarify-0.3.2}/uv.lock +1 -1
  8. jarify-0.3.1/src/jarify/rules/prefer_group_by_all.py +0 -84
  9. {jarify-0.3.1 → jarify-0.3.2}/.agents/skills/add-formatter-rule.md +0 -0
  10. {jarify-0.3.1 → jarify-0.3.2}/.github/workflows/ci.yml +0 -0
  11. {jarify-0.3.1 → jarify-0.3.2}/.github/workflows/prepare-release.yml +0 -0
  12. {jarify-0.3.1 → jarify-0.3.2}/.github/workflows/publish.yml +0 -0
  13. {jarify-0.3.1 → jarify-0.3.2}/.gitignore +0 -0
  14. {jarify-0.3.1 → jarify-0.3.2}/.mise/tasks/release/prepare +0 -0
  15. {jarify-0.3.1 → jarify-0.3.2}/.python-version +0 -0
  16. {jarify-0.3.1 → jarify-0.3.2}/AGENTS.md +0 -0
  17. {jarify-0.3.1 → jarify-0.3.2}/LICENSE +0 -0
  18. {jarify-0.3.1 → jarify-0.3.2}/README.md +0 -0
  19. {jarify-0.3.1 → jarify-0.3.2}/cliff.toml +0 -0
  20. {jarify-0.3.1 → jarify-0.3.2}/docs/editors/README.md +0 -0
  21. {jarify-0.3.1 → jarify-0.3.2}/docs/editors/neovim.md +0 -0
  22. {jarify-0.3.1 → jarify-0.3.2}/docs/sql-style-guide.md +0 -0
  23. {jarify-0.3.1 → jarify-0.3.2}/mise.toml +0 -0
  24. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/__init__.py +0 -0
  25. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/cli.py +0 -0
  26. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/config.py +0 -0
  27. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/formatter.py +0 -0
  28. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/generator.py +0 -0
  29. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/linter.py +0 -0
  30. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/parser.py +0 -0
  31. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/__init__.py +0 -0
  32. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/base.py +0 -0
  33. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/consistent_empty_array.py +0 -0
  34. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/cte_naming.py +0 -0
  35. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/duckdb_prefer_qualify.py +0 -0
  36. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/duckdb_type_style.py +0 -0
  37. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/keyword_case.py +0 -0
  38. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/no_implicit_cross_join.py +0 -0
  39. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/no_select_star.py +0 -0
  40. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/no_select_star_in_cte.py +0 -0
  41. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/no_unused_cte.py +0 -0
  42. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/prefer_using_over_on.py +0 -0
  43. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/rules/trailing_commas.py +0 -0
  44. {jarify-0.3.1 → jarify-0.3.2}/src/jarify/types.py +0 -0
  45. {jarify-0.3.1 → jarify-0.3.2}/tests/__init__.py +0 -0
  46. {jarify-0.3.1 → jarify-0.3.2}/tests/conftest.py +0 -0
  47. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/complex/real_world.expected.sql +0 -0
  48. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/complex/real_world.input.sql +0 -0
  49. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/basic.expected.sql +0 -0
  50. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/basic.input.sql +0 -0
  51. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/inline_primary_key.expected.sql +0 -0
  52. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/inline_primary_key.input.sql +0 -0
  53. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.expected.sql +0 -0
  54. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.input.sql +0 -0
  55. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.expected.sql +0 -0
  56. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.input.sql +0 -0
  57. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.expected.sql +0 -0
  58. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.input.sql +0 -0
  59. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.expected.sql +0 -0
  60. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.input.sql +0 -0
  61. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.expected.sql +0 -0
  62. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.input.sql +0 -0
  63. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.expected.sql +0 -0
  64. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.input.sql +0 -0
  65. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.expected.sql +0 -0
  66. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.input.sql +0 -0
  67. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.expected.sql +0 -0
  68. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.input.sql +0 -0
  69. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.expected.sql +0 -0
  70. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.input.sql +0 -0
  71. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/numeric_decimal_type.expected.sql +0 -0
  72. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/numeric_decimal_type.input.sql +0 -0
  73. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.expected.sql +0 -0
  74. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.input.sql +0 -0
  75. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.expected.sql +0 -0
  76. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.input.sql +0 -0
  77. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.expected.sql +0 -0
  78. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.input.sql +0 -0
  79. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.expected.sql +0 -0
  80. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.input.sql +0 -0
  81. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.expected.sql +0 -0
  82. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.input.sql +0 -0
  83. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.expected.sql +0 -0
  84. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.input.sql +0 -0
  85. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.expected.sql +0 -0
  86. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.input.sql +0 -0
  87. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.expected.sql +0 -0
  88. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.input.sql +0 -0
  89. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.expected.sql +0 -0
  90. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.input.sql +0 -0
  91. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.expected.sql +0 -0
  92. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.input.sql +0 -0
  93. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.expected.sql +0 -0
  94. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.input.sql +0 -0
  95. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/bare_join.expected.sql +0 -0
  96. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/bare_join.input.sql +0 -0
  97. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/comma_join.expected.sql +0 -0
  98. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/comma_join.input.sql +0 -0
  99. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.expected.sql +0 -0
  100. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.input.sql +0 -0
  101. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/multi_join.expected.sql +0 -0
  102. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/multi_join.input.sql +0 -0
  103. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.expected.sql +0 -0
  104. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.input.sql +0 -0
  105. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.expected.sql +0 -0
  106. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.input.sql +0 -0
  107. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.expected.sql +0 -0
  108. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.input.sql +0 -0
  109. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.expected.sql +0 -0
  110. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.input.sql +0 -0
  111. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/basic.expected.sql +0 -0
  112. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/basic.input.sql +0 -0
  113. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.expected.sql +0 -0
  114. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.input.sql +0 -0
  115. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.expected.sql +0 -0
  116. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.input.sql +0 -0
  117. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.expected.sql +0 -0
  118. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.input.sql +0 -0
  119. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.expected.sql +0 -0
  120. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.input.sql +0 -0
  121. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.expected.sql +0 -0
  122. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.input.sql +0 -0
  123. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.expected.sql +0 -0
  124. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.input.sql +0 -0
  125. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.expected.sql +0 -0
  126. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.input.sql +0 -0
  127. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_first.expected.sql +0 -0
  128. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_first.input.sql +0 -0
  129. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.expected.sql +0 -0
  130. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.input.sql +0 -0
  131. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.expected.sql +0 -0
  132. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.input.sql +0 -0
  133. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/group_by_all.expected.sql +0 -0
  134. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/group_by_all.input.sql +0 -0
  135. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/ifnull.expected.sql +0 -0
  136. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/ifnull.input.sql +0 -0
  137. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/inline_comment.expected.sql +0 -0
  138. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/inline_comment.input.sql +0 -0
  139. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.expected.sql +0 -0
  140. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.input.sql +0 -0
  141. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/leading_comment.expected.sql +0 -0
  142. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/leading_comment.input.sql +0 -0
  143. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.expected.sql +0 -0
  144. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.input.sql +0 -0
  145. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/list_contains.expected.sql +0 -0
  146. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/list_contains.input.sql +0 -0
  147. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.expected.sql +0 -0
  148. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.input.sql +0 -0
  149. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_clause.expected.sql +0 -0
  150. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_clause.input.sql +0 -0
  151. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.expected.sql +0 -0
  152. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.input.sql +0 -0
  153. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.expected.sql +0 -0
  154. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.input.sql +0 -0
  155. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.expected.sql +0 -0
  156. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.input.sql +0 -0
  157. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.expected.sql +0 -0
  158. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.input.sql +0 -0
  159. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.expected.sql +0 -0
  160. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.input.sql +0 -0
  161. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.expected.sql +0 -0
  162. {jarify-0.3.1 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.input.sql +0 -0
  163. {jarify-0.3.1 → jarify-0.3.2}/tests/test_config.py +0 -0
  164. {jarify-0.3.1 → jarify-0.3.2}/tests/test_duckdb_rules.py +0 -0
  165. {jarify-0.3.1 → jarify-0.3.2}/tests/test_fixtures.py +0 -0
  166. {jarify-0.3.1 → jarify-0.3.2}/tests/test_formatter.py +0 -0
  167. {jarify-0.3.1 → jarify-0.3.2}/tests/test_lint_rules.py +0 -0
  168. {jarify-0.3.1 → jarify-0.3.2}/tests/test_parser.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-04-27
4
+ ### Bug Fixes
5
+
6
+ - handle mixed agg/non-agg SELECT exprs in prefer-group-by-all ([#198](https://github.com/amfaro/jarify/pull/198)) ([#198](https://github.com/amfaro/jarify/pull/198))
7
+
3
8
  ## [0.3.1] - 2026-04-27
4
9
  ### Bug Fixes
5
10
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarify
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Bespoke SQL linter and formatter for DuckDB, powered by sqlglot
5
5
  Project-URL: Homepage, https://github.com/amfaro/jarify
6
6
  Project-URL: Repository, https://github.com/amfaro/jarify
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jarify"
3
- version = "0.3.1"
3
+ version = "0.3.2"
4
4
  description = "Bespoke SQL linter and formatter for DuckDB, powered by sqlglot"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -0,0 +1,119 @@
1
+ """Rule: rewrite explicit GROUP BY column list to GROUP BY ALL when equivalent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sqlglot.expressions as exp
6
+
7
+ from jarify.rules.base import FormatterRule, _node_pos
8
+ from jarify.types import LintViolation
9
+
10
+
11
+ def _free_col_refs(expr: exp.Expression) -> set[str]:
12
+ """Return SQL strings of Column nodes in *expr* that are not inside any AggFunc.
13
+
14
+ For a purely non-aggregate expression like ``LOWER(name)`` this returns
15
+ ``{"name"}`` (the leaf column, not the whole expression). Callers that
16
+ need the full expression SQL for purely non-aggregate items should call
17
+ ``expr.sql()`` directly instead.
18
+ """
19
+ if isinstance(expr, exp.AggFunc):
20
+ return set()
21
+ if isinstance(expr, exp.Column):
22
+ return {expr.sql(dialect="duckdb")}
23
+ result: set[str] = set()
24
+ for child in expr.args.values():
25
+ if isinstance(child, list):
26
+ for c in child:
27
+ if isinstance(c, exp.Expression):
28
+ result |= _free_col_refs(c)
29
+ elif isinstance(child, exp.Expression):
30
+ result |= _free_col_refs(child)
31
+ return result
32
+
33
+
34
+ def _non_agg_sqls(select: exp.Select) -> set[str]:
35
+ """Derive the set of "non-aggregate expressions" that GROUP BY ALL would use.
36
+
37
+ Rules:
38
+ - If a SELECT item contains **no** AggFunc anywhere, the whole item counts
39
+ as a GROUP BY candidate (e.g. ``LOWER(name)``, ``col1``).
40
+ - If a SELECT item is a **mixed** expression (contains an AggFunc *and*
41
+ free column references outside that AggFunc), those free column refs are
42
+ the GROUP BY candidates (e.g. ``'prefix' || col1 || STRING_AGG(x)``
43
+ contributes ``col1``).
44
+ - Pure aggregate items (e.g. ``SUM(col)`` with no outer column refs)
45
+ contribute nothing.
46
+ """
47
+ result: set[str] = set()
48
+ for item in select.expressions:
49
+ inner = item.this if isinstance(item, exp.Alias) else item
50
+ if not inner.find(exp.AggFunc):
51
+ # Entirely non-aggregate — use the whole expression as-is.
52
+ result.add(inner.sql(dialect="duckdb"))
53
+ else:
54
+ # Mixed or pure aggregate — collect free (non-agg) column refs.
55
+ result |= _free_col_refs(inner)
56
+ return result
57
+
58
+
59
+ class PreferGroupByAllRule(FormatterRule):
60
+ """Format and lint: rewrite explicit GROUP BY col list to GROUP BY ALL when equivalent."""
61
+
62
+ def __init__(self, severity: str = "warn") -> None:
63
+ self.severity = severity
64
+
65
+ @property
66
+ def name(self) -> str:
67
+ return "prefer-group-by-all"
68
+
69
+ def apply(self, tree: exp.Expression) -> exp.Expression:
70
+ """Rewrite GROUP BY <cols> → GROUP BY ALL when all non-agg SELECT cols are covered."""
71
+ for select in tree.find_all(exp.Select):
72
+ group = select.args.get("group")
73
+ if not group or not group.expressions:
74
+ continue
75
+ if group.args.get("all"):
76
+ continue
77
+
78
+ non_agg = _non_agg_sqls(select)
79
+ if not non_agg:
80
+ continue
81
+
82
+ group_sqls = {e.sql(dialect="duckdb") for e in group.expressions}
83
+ if non_agg == group_sqls:
84
+ group.set("all", True)
85
+ group.set("expressions", [])
86
+
87
+ return tree
88
+
89
+ def check(self, tree: exp.Expression) -> list[LintViolation]:
90
+ if self.severity == "off":
91
+ return []
92
+ violations: list[LintViolation] = []
93
+
94
+ for select in tree.find_all(exp.Select):
95
+ group = select.args.get("group")
96
+ if not group or not group.expressions:
97
+ continue
98
+ if group.args.get("all"):
99
+ # Already GROUP BY ALL — nothing to flag
100
+ continue
101
+
102
+ non_agg = _non_agg_sqls(select)
103
+ if not non_agg:
104
+ continue
105
+
106
+ group_sqls = {e.sql(dialect="duckdb") for e in group.expressions}
107
+ if non_agg == group_sqls:
108
+ _line, _col = _node_pos(group)
109
+ violations.append(
110
+ LintViolation(
111
+ rule=self.name,
112
+ severity=self.severity,
113
+ message="All non-aggregated SELECT columns are listed in GROUP BY; prefer GROUP BY ALL",
114
+ line=_line,
115
+ column=_col,
116
+ )
117
+ )
118
+
119
+ return violations
@@ -0,0 +1,5 @@
1
+ SELECT
2
+ 'prefix: ' || group_field || STRING_AGG(transaction_id, ', ') || group_key
3
+ FROM t
4
+ GROUP BY ALL
5
+ ;
@@ -0,0 +1,5 @@
1
+ SELECT
2
+ 'prefix: ' || group_field || string_agg(transaction_id, ', ') || group_key
3
+ FROM t
4
+ GROUP BY group_field, group_key
5
+ ;
@@ -34,7 +34,7 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "jarify"
37
- version = "0.3.1"
37
+ version = "0.3.2"
38
38
  source = { editable = "." }
39
39
  dependencies = [
40
40
  { name = "click" },
@@ -1,84 +0,0 @@
1
- """Rule: rewrite explicit GROUP BY column list to GROUP BY ALL when equivalent."""
2
-
3
- from __future__ import annotations
4
-
5
- import sqlglot.expressions as exp
6
-
7
- from jarify.rules.base import FormatterRule, _node_pos
8
- from jarify.types import LintViolation
9
-
10
-
11
- class PreferGroupByAllRule(FormatterRule):
12
- """Format and lint: rewrite explicit GROUP BY col list to GROUP BY ALL when equivalent."""
13
-
14
- def __init__(self, severity: str = "warn") -> None:
15
- self.severity = severity
16
-
17
- @property
18
- def name(self) -> str:
19
- return "prefer-group-by-all"
20
-
21
- def apply(self, tree: exp.Expression) -> exp.Expression:
22
- """Rewrite GROUP BY <cols> → GROUP BY ALL when all non-agg SELECT cols are covered."""
23
- for select in tree.find_all(exp.Select):
24
- group = select.args.get("group")
25
- if not group or not group.expressions:
26
- continue
27
- if group.args.get("all"):
28
- continue
29
-
30
- non_agg_sqls: list[str] = []
31
- for item in select.expressions:
32
- inner = item.this if isinstance(item, exp.Alias) else item
33
- if not inner.find(exp.AggFunc):
34
- non_agg_sqls.append(inner.sql(dialect="duckdb"))
35
-
36
- if not non_agg_sqls:
37
- continue
38
-
39
- group_sqls = {e.sql(dialect="duckdb") for e in group.expressions}
40
- if set(non_agg_sqls) == group_sqls:
41
- group.set("all", True)
42
- group.set("expressions", [])
43
-
44
- return tree
45
-
46
- def check(self, tree: exp.Expression) -> list[LintViolation]:
47
- if self.severity == "off":
48
- return []
49
- violations: list[LintViolation] = []
50
-
51
- for select in tree.find_all(exp.Select):
52
- group = select.args.get("group")
53
- if not group or not group.expressions:
54
- continue
55
- if group.args.get("all"):
56
- # Already GROUP BY ALL — nothing to flag
57
- continue
58
-
59
- # Collect non-aggregate SELECT expressions (unwrap aliases)
60
- non_agg_sqls: list[str] = []
61
- for item in select.expressions:
62
- inner = item.this if isinstance(item, exp.Alias) else item
63
- if not inner.find(exp.AggFunc):
64
- non_agg_sqls.append(inner.sql(dialect="duckdb"))
65
-
66
- if not non_agg_sqls:
67
- continue
68
-
69
- group_sqls = {e.sql(dialect="duckdb") for e in group.expressions}
70
- non_agg_set = set(non_agg_sqls)
71
-
72
- if non_agg_set and non_agg_set == group_sqls:
73
- _line, _col = _node_pos(group)
74
- violations.append(
75
- LintViolation(
76
- rule=self.name,
77
- severity=self.severity,
78
- message="All non-aggregated SELECT columns are listed in GROUP BY; prefer GROUP BY ALL",
79
- line=_line,
80
- column=_col,
81
- )
82
- )
83
-
84
- return violations
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes