jarify 0.3.2__tar.gz → 0.4.0__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 (172) hide show
  1. {jarify-0.3.2 → jarify-0.4.0}/CHANGELOG.md +10 -0
  2. {jarify-0.3.2 → jarify-0.4.0}/PKG-INFO +1 -1
  3. {jarify-0.3.2 → jarify-0.4.0}/docs/sql-style-guide.md +24 -0
  4. {jarify-0.3.2 → jarify-0.4.0}/pyproject.toml +1 -1
  5. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/config.py +1 -0
  6. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/formatter.py +10 -1
  7. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/generator.py +6 -0
  8. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/__init__.py +3 -0
  9. jarify-0.4.0/src/jarify/rules/prefer_neq_operator.py +53 -0
  10. jarify-0.4.0/tests/fixtures/operators/prefer_neq_operator.expected.sql +7 -0
  11. jarify-0.4.0/tests/fixtures/operators/prefer_neq_operator.input.sql +6 -0
  12. jarify-0.4.0/tests/fixtures/templates/rust_fmt_trailing_placeholder.expected.sql +7 -0
  13. jarify-0.4.0/tests/fixtures/templates/rust_fmt_trailing_placeholder.input.sql +6 -0
  14. {jarify-0.3.2 → jarify-0.4.0}/tests/test_formatter.py +24 -0
  15. {jarify-0.3.2 → jarify-0.4.0}/tests/test_lint_rules.py +17 -0
  16. {jarify-0.3.2 → jarify-0.4.0}/uv.lock +1 -1
  17. {jarify-0.3.2 → jarify-0.4.0}/.agents/skills/add-formatter-rule.md +0 -0
  18. {jarify-0.3.2 → jarify-0.4.0}/.github/workflows/ci.yml +0 -0
  19. {jarify-0.3.2 → jarify-0.4.0}/.github/workflows/prepare-release.yml +0 -0
  20. {jarify-0.3.2 → jarify-0.4.0}/.github/workflows/publish.yml +0 -0
  21. {jarify-0.3.2 → jarify-0.4.0}/.gitignore +0 -0
  22. {jarify-0.3.2 → jarify-0.4.0}/.mise/tasks/release/prepare +0 -0
  23. {jarify-0.3.2 → jarify-0.4.0}/.python-version +0 -0
  24. {jarify-0.3.2 → jarify-0.4.0}/AGENTS.md +0 -0
  25. {jarify-0.3.2 → jarify-0.4.0}/LICENSE +0 -0
  26. {jarify-0.3.2 → jarify-0.4.0}/README.md +0 -0
  27. {jarify-0.3.2 → jarify-0.4.0}/cliff.toml +0 -0
  28. {jarify-0.3.2 → jarify-0.4.0}/docs/editors/README.md +0 -0
  29. {jarify-0.3.2 → jarify-0.4.0}/docs/editors/neovim.md +0 -0
  30. {jarify-0.3.2 → jarify-0.4.0}/mise.toml +0 -0
  31. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/__init__.py +0 -0
  32. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/cli.py +0 -0
  33. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/linter.py +0 -0
  34. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/parser.py +0 -0
  35. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/base.py +0 -0
  36. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/consistent_empty_array.py +0 -0
  37. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/cte_naming.py +0 -0
  38. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/duckdb_prefer_qualify.py +0 -0
  39. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/duckdb_type_style.py +0 -0
  40. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/keyword_case.py +0 -0
  41. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/no_implicit_cross_join.py +0 -0
  42. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/no_select_star.py +0 -0
  43. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/no_select_star_in_cte.py +0 -0
  44. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/no_unused_cte.py +0 -0
  45. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/prefer_group_by_all.py +0 -0
  46. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/prefer_using_over_on.py +0 -0
  47. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/rules/trailing_commas.py +0 -0
  48. {jarify-0.3.2 → jarify-0.4.0}/src/jarify/types.py +0 -0
  49. {jarify-0.3.2 → jarify-0.4.0}/tests/__init__.py +0 -0
  50. {jarify-0.3.2 → jarify-0.4.0}/tests/conftest.py +0 -0
  51. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/complex/real_world.expected.sql +0 -0
  52. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/complex/real_world.input.sql +0 -0
  53. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/basic.expected.sql +0 -0
  54. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/basic.input.sql +0 -0
  55. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/inline_primary_key.expected.sql +0 -0
  56. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/inline_primary_key.input.sql +0 -0
  57. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/simple_no_constraints.expected.sql +0 -0
  58. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/create_table/simple_no_constraints.input.sql +0 -0
  59. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/basic_cte.expected.sql +0 -0
  60. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/basic_cte.input.sql +0 -0
  61. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/cte_inline_comment.expected.sql +0 -0
  62. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/cte_inline_comment.input.sql +0 -0
  63. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/cte_leading_comment.expected.sql +0 -0
  64. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/cte_leading_comment.input.sql +0 -0
  65. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/in_subquery.expected.sql +0 -0
  66. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/cte/in_subquery.input.sql +0 -0
  67. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/agg_window_uppercase.expected.sql +0 -0
  68. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/agg_window_uppercase.input.sql +0 -0
  69. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/create_macro.expected.sql +0 -0
  70. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/create_macro.input.sql +0 -0
  71. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/create_macro_table.expected.sql +0 -0
  72. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/create_macro_table.input.sql +0 -0
  73. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/distinct_in_aggregates.expected.sql +0 -0
  74. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/distinct_in_aggregates.input.sql +0 -0
  75. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/numeric_decimal_type.expected.sql +0 -0
  76. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/numeric_decimal_type.input.sql +0 -0
  77. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/pivot.expected.sql +0 -0
  78. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/pivot.input.sql +0 -0
  79. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/pivot_order_by.expected.sql +0 -0
  80. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/pivot_order_by.input.sql +0 -0
  81. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/qualify.expected.sql +0 -0
  82. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/qualify.input.sql +0 -0
  83. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/read_parquet.expected.sql +0 -0
  84. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/read_parquet.input.sql +0 -0
  85. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/reserved_keyword_type_cast.expected.sql +0 -0
  86. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/reserved_keyword_type_cast.input.sql +0 -0
  87. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/rust_format_syntax.expected.sql +0 -0
  88. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/rust_format_syntax.input.sql +0 -0
  89. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/string_agg.expected.sql +0 -0
  90. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/string_agg.input.sql +0 -0
  91. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/struct_cast_alignment.expected.sql +0 -0
  92. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/struct_cast_alignment.input.sql +0 -0
  93. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/type_cast.expected.sql +0 -0
  94. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/type_cast.input.sql +0 -0
  95. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/values_cte.expected.sql +0 -0
  96. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/duckdb/values_cte.input.sql +0 -0
  97. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/aligned_joins.expected.sql +0 -0
  98. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/aligned_joins.input.sql +0 -0
  99. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/bare_join.expected.sql +0 -0
  100. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/bare_join.input.sql +0 -0
  101. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/comma_join.expected.sql +0 -0
  102. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/comma_join.input.sql +0 -0
  103. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/cross_join_unnest_alias.expected.sql +0 -0
  104. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/cross_join_unnest_alias.input.sql +0 -0
  105. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/multi_join.expected.sql +0 -0
  106. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/multi_join.input.sql +0 -0
  107. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/outer_join_alias_alignment.expected.sql +0 -0
  108. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/outer_join_alias_alignment.input.sql +0 -0
  109. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/unnest_alias_no_as.expected.sql +0 -0
  110. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/joins/unnest_alias_no_as.input.sql +0 -0
  111. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.expected.sql +0 -0
  112. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/alias_alignment_rust_fmt_placeholder.input.sql +0 -0
  113. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/alias_normalization.expected.sql +0 -0
  114. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/alias_normalization.input.sql +0 -0
  115. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/basic.expected.sql +0 -0
  116. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/basic.input.sql +0 -0
  117. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/between_column_comment.expected.sql +0 -0
  118. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/between_column_comment.input.sql +0 -0
  119. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/boolean_literals.expected.sql +0 -0
  120. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/boolean_literals.input.sql +0 -0
  121. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_and_condition.expected.sql +0 -0
  122. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_and_condition.input.sql +0 -0
  123. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_from_first_subquery.expected.sql +0 -0
  124. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_from_first_subquery.input.sql +0 -0
  125. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_inline.expected.sql +0 -0
  126. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_inline.input.sql +0 -0
  127. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_multiline_threshold.expected.sql +0 -0
  128. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/case_when_multiline_threshold.input.sql +0 -0
  129. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/coalesce_to_ifnull.expected.sql +0 -0
  130. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/coalesce_to_ifnull.input.sql +0 -0
  131. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_first.expected.sql +0 -0
  132. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_first.input.sql +0 -0
  133. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_first_star_qualified.expected.sql +0 -0
  134. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_first_star_qualified.input.sql +0 -0
  135. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_inline_subquery.expected.sql +0 -0
  136. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/from_inline_subquery.input.sql +0 -0
  137. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/group_by_all.expected.sql +0 -0
  138. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/group_by_all.input.sql +0 -0
  139. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/group_by_all_mixed_expr.expected.sql +0 -0
  140. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/group_by_all_mixed_expr.input.sql +0 -0
  141. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/ifnull.expected.sql +0 -0
  142. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/ifnull.input.sql +0 -0
  143. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/inline_comment.expected.sql +0 -0
  144. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/inline_comment.input.sql +0 -0
  145. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/json_arrow_operators.expected.sql +0 -0
  146. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/json_arrow_operators.input.sql +0 -0
  147. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/leading_comment.expected.sql +0 -0
  148. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/leading_comment.input.sql +0 -0
  149. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/leading_comment_multiline.expected.sql +0 -0
  150. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/leading_comment_multiline.input.sql +0 -0
  151. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/list_contains.expected.sql +0 -0
  152. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/list_contains.input.sql +0 -0
  153. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/paren_arithmetic.expected.sql +0 -0
  154. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/paren_arithmetic.input.sql +0 -0
  155. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_clause.expected.sql +0 -0
  156. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_clause.input.sql +0 -0
  157. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_eq_alignment.expected.sql +0 -0
  158. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_eq_alignment.input.sql +0 -0
  159. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_eq_alignment_nested_subquery.expected.sql +0 -0
  160. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_eq_alignment_nested_subquery.input.sql +0 -0
  161. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_or_inline.expected.sql +0 -0
  162. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/select/where_or_inline.input.sql +0 -0
  163. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.expected.sql +0 -0
  164. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_consecutive_placeholders.input.sql +0 -0
  165. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.expected.sql +0 -0
  166. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_ctas_body_placeholder.input.sql +0 -0
  167. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_indented_placeholders.expected.sql +0 -0
  168. {jarify-0.3.2 → jarify-0.4.0}/tests/fixtures/templates/rust_fmt_indented_placeholders.input.sql +0 -0
  169. {jarify-0.3.2 → jarify-0.4.0}/tests/test_config.py +0 -0
  170. {jarify-0.3.2 → jarify-0.4.0}/tests/test_duckdb_rules.py +0 -0
  171. {jarify-0.3.2 → jarify-0.4.0}/tests/test_fixtures.py +0 -0
  172. {jarify-0.3.2 → jarify-0.4.0}/tests/test_parser.py +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-04-27
4
+ ### Features
5
+
6
+ - add prefer-neq-operator rule — rewrite <> to != ([#206](https://github.com/amfaro/jarify/pull/206)) ([#206](https://github.com/amfaro/jarify/pull/206))
7
+
8
+ ## [0.3.3] - 2026-04-27
9
+ ### Bug Fixes
10
+
11
+ - place trailing rust fmt placeholder before statement terminator ([#201](https://github.com/amfaro/jarify/pull/201)) ([#201](https://github.com/amfaro/jarify/pull/201))
12
+
3
13
  ## [0.3.2] - 2026-04-27
4
14
  ### Bug Fixes
5
15
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jarify
3
- Version: 0.3.2
3
+ Version: 0.4.0
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
@@ -986,3 +986,27 @@ SELECT
986
986
  FROM _base
987
987
  ;
988
988
  ```
989
+
990
+ ---
991
+
992
+ ### `prefer-neq-operator`
993
+
994
+ Always use `!=` for inequality comparisons instead of the SQL-92 `<>` operator.
995
+ Both are valid in DuckDB, but `!=` is the modern standard.
996
+ The formatter rewrites `<>` to `!=` automatically.
997
+
998
+ **Bad**
999
+ ```sql
1000
+ SELECT a
1001
+ FROM t
1002
+ WHERE x <> y
1003
+ ```
1004
+
1005
+ **Good**
1006
+ ```sql
1007
+ SELECT
1008
+ a
1009
+ FROM t
1010
+ WHERE x != y
1011
+ ;
1012
+ ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "jarify"
3
- version = "0.3.2"
3
+ version = "0.4.0"
4
4
  description = "Bespoke SQL linter and formatter for DuckDB, powered by sqlglot"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -45,6 +45,7 @@ class JarifyConfig:
45
45
  prefer_using_over_on: str = "warn" # suggest USING (col) over ON a.col = b.col
46
46
  consistent_empty_array: str = "warn" # prefer [] over '[]'::type[] empty array
47
47
  no_select_star_in_cte: str = "warn" # flag SELECT * inside CTE bodies
48
+ prefer_neq_operator: str = "warn" # rewrite <> to != inequality operator
48
49
 
49
50
  # --- per-rule overrides (populated from [rules.*] in toml) ---
50
51
  rules: dict[str, Any] = field(default_factory=dict)
@@ -88,11 +88,20 @@ def format_sql(
88
88
  tree = _apply_rules(tree, rules)
89
89
  formatted_parts.append(generator.generate(tree))
90
90
 
91
- formatted = "\n;\n\n".join(formatted_parts) + ("\n;\n" if formatted_parts else "")
91
+ # Join statements but hold off on the trailing ;\n -- it must come *after*
92
+ # no-anchor line placeholders are re-inserted, otherwise a trailing
93
+ # {placeholder} ends up after the semicolon instead of before it, which
94
+ # breaks SQL templates that use the placeholder as an optional WHERE clause.
95
+ # A trailing \n is appended to ensure the last line ends with a newline so
96
+ # that no-anchor placeholder re-insertion does not concatenate directly onto
97
+ # the last SQL token without a line break.
98
+ formatted = "\n;\n\n".join(formatted_parts) + ("\n" if formatted_parts else "")
92
99
  formatted = _unmask_rust_fmt_placeholders(formatted, inline_mask)
93
100
  formatted = _unmask_ifnull(formatted)
94
101
  formatted = _unmask_numeric(formatted)
95
102
  result = _reinsert_line_rust_fmt_placeholders(formatted, line_insertions)
103
+ if formatted_parts:
104
+ result = result.rstrip() + "\n;\n"
96
105
  return _restore_ctas_body_placeholders(result, ctas_body_map), warnings
97
106
 
98
107
 
@@ -862,6 +862,12 @@ class JarifyGenerator(DuckDB.Generator):
862
862
  return super().star_sql(expression)
863
863
 
864
864
  # ------------------------------------------------------------------
865
+ # Inequality operator: always emit != (never <>)
866
+ # ------------------------------------------------------------------
867
+
868
+ def neq_sql(self, expression: exp.NEQ) -> str:
869
+ return self.binary(expression, "!=")
870
+
865
871
  # IS NOT NULL: preserve original form instead of NOT x IS NULL
866
872
  # ------------------------------------------------------------------
867
873
 
@@ -15,6 +15,7 @@ from jarify.rules.no_select_star import NoSelectStarRule
15
15
  from jarify.rules.no_select_star_in_cte import NoSelectStarInCteRule
16
16
  from jarify.rules.no_unused_cte import NoUnusedCteRule
17
17
  from jarify.rules.prefer_group_by_all import PreferGroupByAllRule
18
+ from jarify.rules.prefer_neq_operator import PreferNeqOperatorRule
18
19
  from jarify.rules.prefer_using_over_on import PreferUsingOverOnRule
19
20
  from jarify.rules.trailing_commas import TrailingCommasRule
20
21
 
@@ -58,5 +59,7 @@ def get_default_rules(config: JarifyConfig) -> list[FormatterRule]:
58
59
  rules.append(ConsistentEmptyArrayRule(severity=config.consistent_empty_array))
59
60
  if config.no_select_star_in_cte != "off":
60
61
  rules.append(NoSelectStarInCteRule(severity=config.no_select_star_in_cte))
62
+ if config.prefer_neq_operator != "off":
63
+ rules.append(PreferNeqOperatorRule(severity=config.prefer_neq_operator))
61
64
 
62
65
  return rules
@@ -0,0 +1,53 @@
1
+ """Rule: rewrite <> to != in inequality expressions."""
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 PreferNeqOperatorRule(FormatterRule):
12
+ """Prefer != over <> for inequality comparisons.
13
+
14
+ Both operators are equivalent in DuckDB, but != is the modern standard.
15
+
16
+ **Format**: The JarifyGenerator's ``neq_sql`` override always emits ``!=``,
17
+ so ``apply()`` is a no-op here — the rewrite happens at generation time.
18
+
19
+ **Lint**: Because sqlglot normalises both ``!=`` and ``<>`` to the same
20
+ ``exp.NEQ`` AST node, the lint check cannot distinguish the two from the
21
+ AST alone. The check flags every ``NEQ`` node so that users running
22
+ ``jarify lint`` without first formatting are told to prefer ``!=``.
23
+ False positives (SQL already using ``!=``) are harmless: running
24
+ ``jarify fmt`` confirms the canonical form and silences the flag.
25
+ """
26
+
27
+ def __init__(self, severity: str = "warn") -> None:
28
+ self.severity = severity
29
+
30
+ @property
31
+ def name(self) -> str:
32
+ return "prefer-neq-operator"
33
+
34
+ def apply(self, tree: exp.Expression) -> exp.Expression:
35
+ # The JarifyGenerator.neq_sql override handles the rewrite to `!=`.
36
+ return tree
37
+
38
+ def check(self, tree: exp.Expression) -> list[LintViolation]:
39
+ if self.severity == "off":
40
+ return []
41
+ violations: list[LintViolation] = []
42
+ for node in tree.find_all(exp.NEQ):
43
+ _line, _col = _node_pos(node)
44
+ violations.append(
45
+ LintViolation(
46
+ rule=self.name,
47
+ severity=self.severity,
48
+ message="Prefer != over <> for inequality comparisons",
49
+ line=_line,
50
+ column=_col,
51
+ )
52
+ )
53
+ return violations
@@ -0,0 +1,7 @@
1
+ SELECT
2
+ a
3
+ ,b
4
+ FROM t
5
+ WHERE x != y
6
+ AND p != q
7
+ ;
@@ -0,0 +1,6 @@
1
+ SELECT
2
+ a
3
+ , b
4
+ FROM t
5
+ WHERE x <> y
6
+ AND p <> q
@@ -0,0 +1,7 @@
1
+ SELECT
2
+ request_id
3
+ ,invoice_date
4
+ ,sku_key
5
+ FROM transactions
6
+ {filter}
7
+ ;
@@ -0,0 +1,6 @@
1
+ SELECT
2
+ request_id
3
+ ,invoice_date
4
+ ,sku_key
5
+ FROM transactions
6
+ {filter}
@@ -225,3 +225,27 @@ class TestGroupByPerLine:
225
225
  out, _ = format_sql("SELECT a, count(*) FROM t GROUP BY a")
226
226
  assert "GROUP BY" in out
227
227
  assert "a" in out
228
+
229
+
230
+ class TestTrailingRustFmtPlaceholder:
231
+ """Trailing whole-line Rust format placeholders must land *before* the
232
+ statement-terminating semicolon, not after it.
233
+
234
+ Regression for: formatter inserts ';' between the last SQL clause and a
235
+ trailing {placeholder}, producing invalid SQL when Rust substitutes a
236
+ non-empty value (e.g. 'WHERE seller_key = ...').
237
+ """
238
+
239
+ def test_placeholder_before_semicolon(self):
240
+ sql = "SELECT a, b\nFROM t\n{filter}"
241
+ out, _ = format_sql(sql)
242
+ lines = out.splitlines()
243
+ filter_idx = next(i for i, line in enumerate(lines) if "{filter}" in line)
244
+ semi_idx = next(i for i, line in enumerate(lines) if line.strip() == ";")
245
+ assert filter_idx < semi_idx, f"{{filter}} (line {filter_idx}) must appear before ';' (line {semi_idx}):\n{out}"
246
+
247
+ def test_idempotent(self):
248
+ sql = "SELECT a, b\nFROM t\n{filter}"
249
+ out, _ = format_sql(sql)
250
+ out2, _ = format_sql(out)
251
+ assert out == out2, f"Formatting is not idempotent:\nFirst pass:\n{out}\nSecond pass:\n{out2}"
@@ -308,6 +308,23 @@ class TestViolationPositions:
308
308
  assert v.line is not None, "line should not be None"
309
309
 
310
310
 
311
+ class TestPreferNeqOperator:
312
+ def test_warns_on_diamond_operator(self):
313
+ sql = "SELECT a FROM t WHERE x <> y"
314
+ rules = _lint(sql)
315
+ assert "prefer-neq-operator" in rules
316
+
317
+ def test_warns_on_multiple_neq_expressions(self):
318
+ sql = "SELECT a FROM t WHERE x <> y AND p <> q"
319
+ rules = _lint(sql)
320
+ assert rules.count("prefer-neq-operator") == 2
321
+
322
+ def test_off_disables_rule(self):
323
+ sql = "SELECT a FROM t WHERE x <> y"
324
+ rules = _lint(sql, prefer_neq_operator="off")
325
+ assert "prefer-neq-operator" not in rules
326
+
327
+
311
328
  class TestRustFormatSyntax:
312
329
  """SQL files used as Rust format-string templates must not emit parse errors."""
313
330
 
@@ -34,7 +34,7 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "jarify"
37
- version = "0.3.2"
37
+ version = "0.4.0"
38
38
  source = { editable = "." }
39
39
  dependencies = [
40
40
  { name = "click" },
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