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.
- {jarify-0.3.0 → jarify-0.3.2}/CHANGELOG.md +10 -0
- {jarify-0.3.0 → jarify-0.3.2}/PKG-INFO +1 -1
- {jarify-0.3.0 → jarify-0.3.2}/pyproject.toml +1 -1
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/formatter.py +5 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/parser.py +65 -4
- jarify-0.3.2/src/jarify/rules/prefer_group_by_all.py +119 -0
- jarify-0.3.2/tests/fixtures/create_table/inline_primary_key.expected.sql +7 -0
- jarify-0.3.2/tests/fixtures/duckdb/numeric_decimal_type.expected.sql +8 -0
- jarify-0.3.2/tests/fixtures/duckdb/numeric_decimal_type.input.sql +7 -0
- jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.expected.sql +5 -0
- jarify-0.3.2/tests/fixtures/select/group_by_all_mixed_expr.input.sql +5 -0
- {jarify-0.3.0 → jarify-0.3.2}/uv.lock +1 -1
- jarify-0.3.0/src/jarify/rules/prefer_group_by_all.py +0 -84
- jarify-0.3.0/tests/fixtures/create_table/inline_primary_key.expected.sql +0 -7
- {jarify-0.3.0 → jarify-0.3.2}/.agents/skills/add-formatter-rule.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/ci.yml +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/prepare-release.yml +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.github/workflows/publish.yml +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.gitignore +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.mise/tasks/release/prepare +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/.python-version +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/AGENTS.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/LICENSE +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/README.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/cliff.toml +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/docs/editors/README.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/docs/editors/neovim.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/docs/sql-style-guide.md +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/mise.toml +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/__init__.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/cli.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/config.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/generator.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/linter.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/__init__.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/base.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/consistent_empty_array.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/cte_naming.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/duckdb_prefer_qualify.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/duckdb_type_style.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/keyword_case.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_implicit_cross_join.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_select_star.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_select_star_in_cte.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/no_unused_cte.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/prefer_using_over_on.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/rules/trailing_commas.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/src/jarify/types.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/__init__.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/conftest.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/complex/real_world.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/complex/real_world.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/basic.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/basic.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/inline_primary_key.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/basic_cte.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_inline_comment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/cte_leading_comment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/cte/in_subquery.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/agg_window_uppercase.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/create_macro_table.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/distinct_in_aggregates.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/pivot_order_by.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/qualify.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/read_parquet.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/reserved_keyword_type_cast.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/rust_format_syntax.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/string_agg.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/struct_cast_alignment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/type_cast.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/duckdb/values_cte.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/aligned_joins.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/bare_join.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/bare_join.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/comma_join.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/comma_join.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/cross_join_unnest_alias.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/multi_join.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/multi_join.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/outer_join_alias_alignment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/joins/unnest_alias_no_as.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_normalization.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/basic.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/basic.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/between_column_comment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/boolean_literals.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_and_condition.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_inline.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/coalesce_to_ifnull.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_first_star_qualified.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/from_inline_subquery.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/group_by_all.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/group_by_all.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/ifnull.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/ifnull.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/inline_comment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/inline_comment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/json_arrow_operators.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/leading_comment_multiline.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/list_contains.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/list_contains.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/paren_arithmetic.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_clause.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_clause.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_or_inline.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.expected.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.input.sql +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/test_config.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/test_duckdb_rules.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/test_fixtures.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/test_formatter.py +0 -0
- {jarify-0.3.0 → jarify-0.3.2}/tests/test_lint_rules.py +0 -0
- {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
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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
|
|
@@ -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
|
|
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
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/create_table/simple_no_constraints.expected.sql
RENAMED
|
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
|
|
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
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.input.sql
RENAMED
|
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
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_from_first_subquery.expected.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/case_when_multiline_threshold.expected.sql
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.expected.sql
RENAMED
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/select/where_eq_alignment_nested_subquery.input.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.input.sql
RENAMED
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.expected.sql
RENAMED
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.input.sql
RENAMED
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.expected.sql
RENAMED
|
File without changes
|
{jarify-0.3.0 → jarify-0.3.2}/tests/fixtures/templates/rust_fmt_indented_placeholders.input.sql
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|