jarify 0.3.0__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 (169) hide show
  1. {jarify-0.3.0 → jarify-0.3.2}/CHANGELOG.md +10 -0
  2. {jarify-0.3.0 → jarify-0.3.2}/PKG-INFO +1 -1
  3. {jarify-0.3.0 → jarify-0.3.2}/pyproject.toml +1 -1
  4. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/formatter.py +5 -0
  5. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/parser.py +65 -4
  6. jarify-0.3.2/src/jarify/rules/prefer_group_by_all.py +119 -0
  7. jarify-0.3.2/tests/fixtures/create_table/inline_primary_key.expected.sql +7 -0
  8. jarify-0.3.2/tests/fixtures/duckdb/numeric_decimal_type.expected.sql +8 -0
  9. jarify-0.3.2/tests/fixtures/duckdb/numeric_decimal_type.input.sql +7 -0
  10. jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.expected.sql +5 -0
  11. jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.input.sql +5 -0
  12. {jarify-0.3.0 → jarify-0.3.2}/uv.lock +1 -1
  13. jarify-0.3.0/src/jarify/rules/prefer_group_by_all.py +0 -84
  14. jarify-0.3.0/tests/fixtures/create_table/inline_primary_key.expected.sql +0 -7
  15. {jarify-0.3.0 → jarify-0.3.2}/.agents/skills/add-formatter-rule.md +0 -0
  16. {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/ci.yml +0 -0
  17. {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/prepare-release.yml +0 -0
  18. {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/publish.yml +0 -0
  19. {jarify-0.3.0 → jarify-0.3.2}/.gitignore +0 -0
  20. {jarify-0.3.0 → jarify-0.3.2}/.mise/tasks/release/prepare +0 -0
  21. {jarify-0.3.0 → jarify-0.3.2}/.python-version +0 -0
  22. {jarify-0.3.0 → jarify-0.3.2}/AGENTS.md +0 -0
  23. {jarify-0.3.0 → jarify-0.3.2}/LICENSE +0 -0
  24. {jarify-0.3.0 → jarify-0.3.2}/README.md +0 -0
  25. {jarify-0.3.0 → jarify-0.3.2}/cliff.toml +0 -0
  26. {jarify-0.3.0 → jarify-0.3.2}/docs/editors/README.md +0 -0
  27. {jarify-0.3.0 → jarify-0.3.2}/docs/editors/neovim.md +0 -0
  28. {jarify-0.3.0 → jarify-0.3.2}/docs/sql-style-guide.md +0 -0
  29. {jarify-0.3.0 → jarify-0.3.2}/mise.toml +0 -0
  30. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/__init__.py +0 -0
  31. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/cli.py +0 -0
  32. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/config.py +0 -0
  33. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/generator.py +0 -0
  34. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/linter.py +0 -0
  35. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/__init__.py +0 -0
  36. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/base.py +0 -0
  37. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/consistent_empty_array.py +0 -0
  38. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/cte_naming.py +0 -0
  39. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/duckdb_prefer_qualify.py +0 -0
  40. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/duckdb_type_style.py +0 -0
  41. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/keyword_case.py +0 -0
  42. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_implicit_cross_join.py +0 -0
  43. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_select_star.py +0 -0
  44. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_select_star_in_cte.py +0 -0
  45. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_unused_cte.py +0 -0
  46. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/prefer_using_over_on.py +0 -0
  47. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/trailing_commas.py +0 -0
  48. {jarify-0.3.0 → jarify-0.3.2}/src/jarify/types.py +0 -0
  49. {jarify-0.3.0 → jarify-0.3.2}/tests/__init__.py +0 -0
  50. {jarify-0.3.0 → jarify-0.3.2}/tests/conftest.py +0 -0
  51. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/complex/real_world.expected.sql +0 -0
  52. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/complex/real_world.input.sql +0 -0
  53. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/basic.expected.sql +0 -0
  54. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/basic.input.sql +0 -0
  55. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/inline_primary_key.input.sql +0 -0
  56. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.expected.sql +0 -0
  57. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.input.sql +0 -0
  58. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.expected.sql +0 -0
  59. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.input.sql +0 -0
  60. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.expected.sql +0 -0
  61. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.input.sql +0 -0
  62. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.expected.sql +0 -0
  63. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.input.sql +0 -0
  64. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.expected.sql +0 -0
  65. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.input.sql +0 -0
  66. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.expected.sql +0 -0
  67. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.input.sql +0 -0
  68. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.expected.sql +0 -0
  69. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.input.sql +0 -0
  70. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.expected.sql +0 -0
  71. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.input.sql +0 -0
  72. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.expected.sql +0 -0
  73. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.input.sql +0 -0
  74. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.expected.sql +0 -0
  75. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.input.sql +0 -0
  76. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.expected.sql +0 -0
  77. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.input.sql +0 -0
  78. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.expected.sql +0 -0
  79. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.input.sql +0 -0
  80. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.expected.sql +0 -0
  81. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.input.sql +0 -0
  82. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.expected.sql +0 -0
  83. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.input.sql +0 -0
  84. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.expected.sql +0 -0
  85. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.input.sql +0 -0
  86. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.expected.sql +0 -0
  87. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.input.sql +0 -0
  88. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.expected.sql +0 -0
  89. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.input.sql +0 -0
  90. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.expected.sql +0 -0
  91. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.input.sql +0 -0
  92. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.expected.sql +0 -0
  93. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.input.sql +0 -0
  94. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.expected.sql +0 -0
  95. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.input.sql +0 -0
  96. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/bare_join.expected.sql +0 -0
  97. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/bare_join.input.sql +0 -0
  98. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/comma_join.expected.sql +0 -0
  99. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/comma_join.input.sql +0 -0
  100. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.expected.sql +0 -0
  101. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.input.sql +0 -0
  102. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/multi_join.expected.sql +0 -0
  103. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/multi_join.input.sql +0 -0
  104. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.expected.sql +0 -0
  105. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.input.sql +0 -0
  106. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.expected.sql +0 -0
  107. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.input.sql +0 -0
  108. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.expected.sql +0 -0
  109. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.input.sql +0 -0
  110. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.expected.sql +0 -0
  111. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.input.sql +0 -0
  112. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/basic.expected.sql +0 -0
  113. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/basic.input.sql +0 -0
  114. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.expected.sql +0 -0
  115. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.input.sql +0 -0
  116. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.expected.sql +0 -0
  117. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.input.sql +0 -0
  118. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.expected.sql +0 -0
  119. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.input.sql +0 -0
  120. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.expected.sql +0 -0
  121. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.input.sql +0 -0
  122. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.expected.sql +0 -0
  123. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.input.sql +0 -0
  124. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.expected.sql +0 -0
  125. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.input.sql +0 -0
  126. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.expected.sql +0 -0
  127. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.input.sql +0 -0
  128. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first.expected.sql +0 -0
  129. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first.input.sql +0 -0
  130. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.expected.sql +0 -0
  131. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.input.sql +0 -0
  132. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.expected.sql +0 -0
  133. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.input.sql +0 -0
  134. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/group_by_all.expected.sql +0 -0
  135. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/group_by_all.input.sql +0 -0
  136. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/ifnull.expected.sql +0 -0
  137. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/ifnull.input.sql +0 -0
  138. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/inline_comment.expected.sql +0 -0
  139. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/inline_comment.input.sql +0 -0
  140. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.expected.sql +0 -0
  141. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.input.sql +0 -0
  142. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment.expected.sql +0 -0
  143. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment.input.sql +0 -0
  144. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.expected.sql +0 -0
  145. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.input.sql +0 -0
  146. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/list_contains.expected.sql +0 -0
  147. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/list_contains.input.sql +0 -0
  148. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.expected.sql +0 -0
  149. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.input.sql +0 -0
  150. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_clause.expected.sql +0 -0
  151. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_clause.input.sql +0 -0
  152. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.expected.sql +0 -0
  153. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.input.sql +0 -0
  154. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.expected.sql +0 -0
  155. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.input.sql +0 -0
  156. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.expected.sql +0 -0
  157. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.input.sql +0 -0
  158. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.expected.sql +0 -0
  159. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.input.sql +0 -0
  160. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.expected.sql +0 -0
  161. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.input.sql +0 -0
  162. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.expected.sql +0 -0
  163. {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.input.sql +0 -0
  164. {jarify-0.3.0 → jarify-0.3.2}/tests/test_config.py +0 -0
  165. {jarify-0.3.0 → jarify-0.3.2}/tests/test_duckdb_rules.py +0 -0
  166. {jarify-0.3.0 → jarify-0.3.2}/tests/test_fixtures.py +0 -0
  167. {jarify-0.3.0 → jarify-0.3.2}/tests/test_formatter.py +0 -0
  168. {jarify-0.3.0 → jarify-0.3.2}/tests/test_lint_rules.py +0 -0
  169. {jarify-0.3.0 → jarify-0.3.2}/tests/test_parser.py +0 -0
@@ -1,5 +1,15 @@
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
+
8
+ ## [0.3.1] - 2026-04-27
9
+ ### Bug Fixes
10
+
11
+ - preserve bare numeric/decimal without injecting default precision ([#195](https://github.com/amfaro/jarify/pull/195)) ([#195](https://github.com/amfaro/jarify/pull/195))
12
+
3
13
  ## [0.3.0] - 2026-04-27
4
14
  ### Features
5
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarify
3
- Version: 0.3.0
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.0"
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"
@@ -14,10 +14,12 @@ from jarify.parser import (
14
14
  _extract_ctas_body_placeholders,
15
15
  _extract_line_rust_fmt_placeholders,
16
16
  _mask_ifnull,
17
+ _mask_numeric,
17
18
  _mask_rust_fmt_placeholders,
18
19
  _reinsert_line_rust_fmt_placeholders,
19
20
  _restore_ctas_body_placeholders,
20
21
  _unmask_ifnull,
22
+ _unmask_numeric,
21
23
  _unmask_rust_fmt_placeholders,
22
24
  parse_sql,
23
25
  )
@@ -59,6 +61,7 @@ def format_sql(
59
61
  stripped_sql, line_insertions = _extract_line_rust_fmt_placeholders(processed_sql)
60
62
  masked_sql, inline_mask = _mask_rust_fmt_placeholders(stripped_sql)
61
63
  masked_sql = _mask_ifnull(masked_sql)
64
+ masked_sql = _mask_numeric(masked_sql)
62
65
 
63
66
  try:
64
67
  trees = parse_sql(masked_sql, dialect=config.dialect)
@@ -67,6 +70,7 @@ def format_sql(
67
70
  if result is not None:
68
71
  result = _unmask_rust_fmt_placeholders(result, inline_mask)
69
72
  result = _unmask_ifnull(result)
73
+ result = _unmask_numeric(result)
70
74
  result = _reinsert_line_rust_fmt_placeholders(result, line_insertions)
71
75
  return _restore_ctas_body_placeholders(result, ctas_body_map), warnings
72
76
  warnings.append(FormatWarning(f"could not parse SQL (formatting skipped): {exc}"))
@@ -87,6 +91,7 @@ def format_sql(
87
91
  formatted = "\n;\n\n".join(formatted_parts) + ("\n;\n" if formatted_parts else "")
88
92
  formatted = _unmask_rust_fmt_placeholders(formatted, inline_mask)
89
93
  formatted = _unmask_ifnull(formatted)
94
+ formatted = _unmask_numeric(formatted)
90
95
  result = _reinsert_line_rust_fmt_placeholders(formatted, line_insertions)
91
96
  return _restore_ctas_body_placeholders(result, ctas_body_map), warnings
92
97
 
@@ -4,13 +4,40 @@ from __future__ import annotations
4
4
 
5
5
  import functools
6
6
  import re
7
+ import typing
7
8
 
8
9
  import sqlglot
10
+ import sqlglot.expressions as _exp
11
+ from sqlglot.dialects.duckdb import DuckDB as _DuckDB
9
12
  from sqlglot.errors import ParseError
10
13
  from sqlglot.expressions import Expression
11
14
 
12
15
  DUCKDB_DIALECT = "duckdb"
13
16
 
17
+ # ---------------------------------------------------------------------------
18
+ # Custom DuckDB dialect — preserve bare NUMERIC/DECIMAL without injecting
19
+ # default precision/scale into the AST.
20
+ # ---------------------------------------------------------------------------
21
+ # sqlglot's DuckDB parser registers a TYPE_CONVERTERS handler for DECIMAL that
22
+ # calls build_default_decimal_type(precision=18, scale=3). This fires for any
23
+ # bare NUMERIC or DECIMAL type (no user-supplied params) and injects (18, 3)
24
+ # into the AST, causing jarify to regenerate `decimal(18, 3)` when the
25
+ # original SQL only said `numeric` or `decimal`. We remove that converter so
26
+ # bare types stay bare and round-trip cleanly.
27
+
28
+
29
+ class _JarifyDuckDBParser(_DuckDB.Parser):
30
+ TYPE_CONVERTERS: typing.ClassVar = {
31
+ k: v for k, v in _DuckDB.Parser.TYPE_CONVERTERS.items() if k != _exp.DType.DECIMAL
32
+ }
33
+
34
+
35
+ class _JarifyDuckDB(_DuckDB):
36
+ Parser = _JarifyDuckDBParser
37
+
38
+
39
+ _JARIFY_DIALECT: type[_DuckDB] = _JarifyDuckDB
40
+
14
41
  # ---------------------------------------------------------------------------
15
42
  # Rust format-string placeholder handling
16
43
  # ---------------------------------------------------------------------------
@@ -260,6 +287,29 @@ def _unmask_ifnull(sql: str) -> str:
260
287
  return sql.replace(_IFNULL_SENTINEL, "ifnull")
261
288
 
262
289
 
290
+ # ---------------------------------------------------------------------------
291
+ # numeric preservation
292
+ # ---------------------------------------------------------------------------
293
+ # sqlglot's DuckDB dialect normalises NUMERIC to DType.DECIMAL at parse time,
294
+ # losing the original keyword. We mask it with a same-length sentinel so the
295
+ # original spelling is preserved through the format round-trip.
296
+ #
297
+ # Sentinel is 7 chars (same as "numeric") so type-column alignment in CREATE
298
+ # TABLE is not disturbed before the unmask.
299
+ _NUMERIC_SENTINEL = "_numer_"
300
+ _NUMERIC_RE = re.compile(r"\bnumeric\b", re.IGNORECASE)
301
+
302
+
303
+ def _mask_numeric(sql: str) -> str:
304
+ """Replace numeric with a sentinel so sqlglot doesn't normalise it to decimal."""
305
+ return _NUMERIC_RE.sub(_NUMERIC_SENTINEL, sql)
306
+
307
+
308
+ def _unmask_numeric(sql: str) -> str:
309
+ """Restore numeric from its sentinel."""
310
+ return sql.replace(_NUMERIC_SENTINEL, "numeric")
311
+
312
+
263
313
  # ---------------------------------------------------------------------------
264
314
  # Reserved-keyword type-cast pre-processor
265
315
  # ---------------------------------------------------------------------------
@@ -316,6 +366,16 @@ def _quote_reserved_cast_types(sql: str) -> str:
316
366
  return _reserved_cast_re().sub(_replacer, sql)
317
367
 
318
368
 
369
+ def _read_dialect(dialect: str) -> type[_DuckDB] | str:
370
+ """Return the parser dialect to use for *dialect*.
371
+
372
+ For the DuckDB dialect we substitute our custom subclass that preserves
373
+ bare NUMERIC/DECIMAL types without injecting default precision/scale.
374
+ All other dialects are passed through unchanged.
375
+ """
376
+ return _JARIFY_DIALECT if dialect == DUCKDB_DIALECT else dialect
377
+
378
+
319
379
  def parse_sql(sql: str, dialect: str = DUCKDB_DIALECT) -> list[Expression]:
320
380
  """Parse a SQL string into a list of sqlglot expression trees.
321
381
 
@@ -324,23 +384,24 @@ def parse_sql(sql: str, dialect: str = DUCKDB_DIALECT) -> list[Expression]:
324
384
  ``::filter[]``) so sqlglot can handle them.
325
385
  """
326
386
  preprocessed = _quote_reserved_cast_types(sql)
327
- return sqlglot.parse(preprocessed, read=dialect, error_level=sqlglot.ErrorLevel.RAISE)
387
+ return sqlglot.parse(preprocessed, read=_read_dialect(dialect), error_level=sqlglot.ErrorLevel.RAISE)
328
388
 
329
389
 
330
390
  def parse_sql_lenient(sql: str, dialect: str = DUCKDB_DIALECT) -> tuple[list[Expression], list[ParseError]]:
331
391
  """Parse SQL leniently, collecting errors instead of raising."""
332
392
  preprocessed = _quote_reserved_cast_types(sql)
393
+ read = _read_dialect(dialect)
333
394
  errors: list[ParseError] = []
334
395
  try:
335
- trees = sqlglot.parse(preprocessed, read=dialect, error_level=sqlglot.ErrorLevel.RAISE)
396
+ trees = sqlglot.parse(preprocessed, read=read, error_level=sqlglot.ErrorLevel.RAISE)
336
397
  except ParseError:
337
398
  # Fall back to lenient parsing so we still get partial trees.
338
399
  # Use IGNORE (not WARN) to suppress sqlglot's side-effect stderr prints;
339
400
  # errors are re-captured via the RAISE pass below.
340
- trees = sqlglot.parse(preprocessed, read=dialect, error_level=sqlglot.ErrorLevel.IGNORE)
401
+ trees = sqlglot.parse(preprocessed, read=read, error_level=sqlglot.ErrorLevel.IGNORE)
341
402
  # Re-parse to capture the actual errors
342
403
  try:
343
- sqlglot.parse(preprocessed, read=dialect, error_level=sqlglot.ErrorLevel.RAISE)
404
+ sqlglot.parse(preprocessed, read=read, error_level=sqlglot.ErrorLevel.RAISE)
344
405
  except ParseError as e:
345
406
  errors.append(e)
346
407
  return trees, errors
@@ -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,7 @@
1
+ CREATE TABLE orders
2
+ (
3
+ order_id int NOT NULL PRIMARY KEY
4
+ ,customer text NOT NULL
5
+ ,total decimal
6
+ )
7
+ ;
@@ -0,0 +1,8 @@
1
+ SELECT
2
+ x::numeric
3
+ ,x::decimal
4
+ ,x::decimal(10, 2)
5
+ ,x::numeric(8, 4)
6
+ ,(ir.rate_data->>'value')::numeric
7
+ FROM t
8
+ ;
@@ -0,0 +1,7 @@
1
+ SELECT
2
+ x::numeric
3
+ ,x::decimal
4
+ ,x::decimal(10, 2)
5
+ ,x::numeric(8, 4)
6
+ ,(ir.rate_data->>'value')::numeric
7
+ FROM t
@@ -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.0"
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
@@ -1,7 +0,0 @@
1
- CREATE TABLE orders
2
- (
3
- order_id int NOT NULL PRIMARY KEY
4
- ,customer text NOT NULL
5
- ,total decimal(18, 3)
6
- )
7
- ;
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