sarj-python-lint 1.84.0__tar.gz → 1.85.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 (301) hide show
  1. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/PKG-INFO +1 -1
  2. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/pyproject.toml +1 -1
  3. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix.py +2 -0
  4. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_file_context.py +2 -0
  5. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/_postgres.py +17 -0
  6. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_registry.py +18 -0
  7. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/_store_sql.py +245 -0
  8. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py +93 -45
  9. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +237 -0
  10. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +469 -0
  11. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py +112 -54
  12. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_recording_store.py +276 -0
  13. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_redundant_sql_alias.py +164 -0
  14. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/prefer_application_model_joins.py +138 -0
  15. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +125 -2
  16. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/prefer_optional_composite.py +297 -0
  17. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +105 -0
  18. sarj_python_lint-1.85.0/src/sarj_python_lint/rules/store_mutation_command_fanout.py +172 -0
  19. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/.gitignore +0 -0
  20. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/LICENSE +0 -0
  21. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/README.md +0 -0
  22. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/__init__.py +0 -0
  23. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/__main__.py +0 -0
  24. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_analysis_session.py +0 -0
  25. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
  26. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
  27. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
  28. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
  29. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
  30. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
  31. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
  32. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
  33. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
  34. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
  35. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
  36. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_security.py +0 -0
  37. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
  38. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
  39. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_filesystem.py +0 -0
  40. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_fixes.py +0 -0
  41. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_parallel.py +0 -0
  42. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_python_target.py +0 -0
  43. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
  44. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_secret_names.py +0 -0
  45. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_source.py +0 -0
  46. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_version.py +0 -0
  47. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/application_logging.py +0 -0
  48. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/autofix.py +0 -0
  49. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/benchmark.py +0 -0
  50. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/cast_access.py +0 -0
  51. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
  52. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/json_boundary.py +0 -0
  53. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/py.typed +0 -0
  54. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/ratchet.py +0 -0
  55. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rule_base.py +0 -0
  56. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/__init__.py +0 -0
  57. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
  58. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
  59. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
  60. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
  61. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
  62. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
  63. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
  64. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
  65. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
  66. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_comments.py +0 -0
  67. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
  68. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
  69. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
  70. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
  71. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
  72. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
  73. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_exports.py +0 -0
  74. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
  75. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
  76. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
  77. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_imports.py +0 -0
  78. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
  79. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
  80. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
  81. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
  82. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_logging.py +0 -0
  83. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
  84. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
  85. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
  86. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
  87. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
  88. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
  89. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
  90. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_paths.py +0 -0
  91. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
  92. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
  93. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
  94. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
  95. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
  96. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
  97. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
  98. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
  99. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
  100. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
  101. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql.py +0 -0
  102. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
  103. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
  104. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
  105. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
  106. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
  107. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
  108. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
  109. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
  110. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
  111. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
  112. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
  113. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
  114. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
  115. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py +0 -0
  116. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
  117. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/cancel_owned_task_without_join.py +0 -0
  118. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py +0 -0
  119. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/delete_route_returns_no_content.py +0 -0
  120. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
  121. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
  122. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
  123. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
  124. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
  125. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
  126. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
  127. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
  128. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
  129. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
  130. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
  131. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
  132. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
  133. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
  134. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py +0 -0
  135. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/negative_only_http_status_assertion.py +0 -0
  136. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py +0 -0
  137. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
  138. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
  139. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_before_validator_input_mutation.py +0 -0
  140. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
  141. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
  142. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
  143. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py +0 -0
  144. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_conftest_test_module_import.py +0 -0
  145. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
  146. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
  147. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py +0 -0
  148. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_create_verb_in_post_path.py +0 -0
  149. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
  150. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
  151. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
  152. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
  153. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py +0 -0
  154. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
  155. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py +0 -0
  156. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py +0 -0
  157. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py +0 -0
  158. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
  159. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
  160. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_file_level_escape_hatch_suppression.py +0 -0
  161. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_first_party_private_import.py +0 -0
  162. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py +0 -0
  163. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py +0 -0
  164. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py +0 -0
  165. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py +0 -0
  166. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_generic_single_export_module.py +0 -0
  167. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
  168. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
  169. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py +0 -0
  170. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
  171. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
  172. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
  173. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py +0 -0
  174. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
  175. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py +0 -0
  176. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
  177. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
  178. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py +0 -0
  179. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
  180. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py +0 -0
  181. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
  182. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
  183. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
  184. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py +0 -0
  185. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_random_uuid_in_sql.py +0 -0
  186. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_raw_connection_in_tests.py +0 -0
  187. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py +0 -0
  188. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
  189. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
  190. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py +0 -0
  191. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
  192. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
  193. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
  194. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
  195. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
  196. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_service_behavior_in_settings.py +0 -0
  197. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_shallow_container_type_guard.py +0 -0
  198. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_shallow_record_type_guard.py +0 -0
  199. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py +0 -0
  200. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_string_concat_in_loop.py +0 -0
  201. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
  202. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
  203. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
  204. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
  205. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unique_violation_message_match.py +0 -0
  206. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py +0 -0
  207. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
  208. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
  209. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py +0 -0
  210. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
  211. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
  212. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py +0 -0
  213. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_work_verb_get_route.py +0 -0
  214. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py +0 -0
  215. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
  216. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
  217. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py +0 -0
  218. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
  219. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
  220. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
  221. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py +0 -0
  222. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
  223. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
  224. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
  225. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
  226. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
  227. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
  228. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py +0 -0
  229. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
  230. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
  231. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
  232. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
  233. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
  234. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py +0 -0
  235. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py +0 -0
  236. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py +0 -0
  237. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
  238. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
  239. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
  240. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
  241. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
  242. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
  243. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
  244. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
  245. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
  246. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
  247. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
  248. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
  249. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
  250. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
  251. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
  252. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
  253. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
  254. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
  255. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
  256. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
  257. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
  258. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py +0 -0
  259. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
  260. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
  261. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
  262. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
  263. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py +0 -0
  264. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py +0 -0
  265. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
  266. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
  267. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
  268. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
  269. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py +0 -0
  270. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
  271. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
  272. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
  273. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py +0 -0
  274. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
  275. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
  276. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
  277. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
  278. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py +0 -0
  279. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py +0 -0
  280. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
  281. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
  282. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
  283. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_pydantic_for_external_json.py +0 -0
  284. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py +0 -0
  285. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py +0 -0
  286. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py +0 -0
  287. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
  288. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
  289. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
  290. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py +0 -0
  291. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py +0 -0
  292. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py +0 -0
  293. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
  294. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
  295. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
  296. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
  297. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
  298. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
  299. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
  300. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
  301. {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/suppression_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sarj-python-lint
3
- Version: 1.84.0
3
+ Version: 1.85.0
4
4
  Summary: Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults
5
5
  Project-URL: Homepage, https://code-standards.sarj.ai/rules/python/
6
6
  Project-URL: Documentation, https://code-standards.sarj.ai/rules/python/
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sarj-python-lint"
3
- version = "1.84.0"
3
+ version = "1.85.0"
4
4
  description = "Custom Python lint rules — AST-based, pre-commit-friendly, hypermodern defaults"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "sarj-ai" }]
@@ -759,6 +759,8 @@ def _primitive_set(node: ast.expr) -> bool:
759
759
 
760
760
 
761
761
  def _dispatch_fix(source: _Source, diagnostic: Diagnostic) -> list[TextEdit]:
762
+ if diagnostic.code == "SARJ080" and diagnostic.severity is not Severity.ERROR:
763
+ return []
762
764
  for root in source.context.nodes(ast.If):
763
765
  if not _at(root, diagnostic) or source.comments_between(root.lineno, _last_line(root)):
764
766
  continue
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
22
22
 
23
23
  from sarj_python_lint.rules._copy_on_write import CopyOnWriteAnalysis
24
24
  from sarj_python_lint.rules._resource_provenance import ResourceProvenance
25
+ from sarj_python_lint.rules._store_sql import StoreQuery
25
26
  from sarj_python_lint.rules._test_provenance import TestProvenance
26
27
 
27
28
 
@@ -31,6 +32,7 @@ class PythonFileContext:
31
32
  self.path = path
32
33
  self.source = source
33
34
  self.session = session if session is not None else AnalysisSession()
35
+ self.store_queries: tuple[StoreQuery, ...] | None = None
34
36
 
35
37
  @cached_property
36
38
  def symbol_source(self) -> str:
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import override
4
+
5
+ from sqlglot import exp, parse
6
+ from sqlglot.dialects.postgres import Postgres
7
+
8
+
9
+ def parse_postgres(source: str) -> list[exp.Expr | None]:
10
+ return parse(source, read=_PostgresForLint)
11
+
12
+
13
+ class _PostgresForLint(Postgres):
14
+ class Parser(Postgres.Parser):
15
+ @override
16
+ def _warn_unsupported(self) -> None:
17
+ """Unsupported input must not copy private SQL into dependency logs."""
@@ -228,7 +228,9 @@ from sarj_python_lint.rules.nan_hidden_by_builtin_min import NanHiddenByBuiltinM
228
228
  from sarj_python_lint.rules.no_conflicting_pydantic_alias_validation import NoConflictingPydanticAliasValidation
229
229
  from sarj_python_lint.rules.no_contradictory_type_guard import NoContradictoryTypeGuard
230
230
  from sarj_python_lint.rules.no_create_verb_in_post_path import NoCreateVerbInPostPath
231
+ from sarj_python_lint.rules.no_custom_control_flow_exception import NoCustomControlFlowException
231
232
  from sarj_python_lint.rules.no_defaulted_environment_setting import NoDefaultedEnvironmentSetting
233
+ from sarj_python_lint.rules.no_discarded_tagged_outcome import NoDiscardedTaggedOutcome
232
234
  from sarj_python_lint.rules.no_duplicate_field_validator_body import NoDuplicateFieldValidatorBody
233
235
  from sarj_python_lint.rules.no_exception_text_in_500_detail import NoExceptionTextIn500Detail
234
236
  from sarj_python_lint.rules.no_folded_bad_request_not_found import NoFoldedBadRequestNotFound
@@ -237,15 +239,19 @@ from sarj_python_lint.rules.no_impossible_pydantic_field_bounds import NoImpossi
237
239
  from sarj_python_lint.rules.no_invalid_pydantic_bypass import NoInvalidPydanticBypass
238
240
  from sarj_python_lint.rules.no_invalid_pydantic_validator_return import NoInvalidPydanticValidatorReturn
239
241
  from sarj_python_lint.rules.no_partial_type_is import NoPartialTypeIs
242
+ from sarj_python_lint.rules.no_recording_store import NoRecordingStore
240
243
  from sarj_python_lint.rules.no_redundant_local_alias import NoRedundantLocalAlias
244
+ from sarj_python_lint.rules.no_redundant_sql_alias import NoRedundantSqlAlias
241
245
  from sarj_python_lint.rules.no_shallow_record_type_guard import NoShallowRecordTypeGuard
242
246
  from sarj_python_lint.rules.no_slash_collection_root_under_prefix import NoSlashCollectionRootUnderPrefix
243
247
  from sarj_python_lint.rules.no_unknown_pydantic_validator_field import NoUnknownPydanticValidatorField
244
248
  from sarj_python_lint.rules.no_work_verb_get_route import NoWorkVerbGetRoute
249
+ from sarj_python_lint.rules.prefer_application_model_joins import PreferApplicationModelJoins
245
250
  from sarj_python_lint.rules.prefer_bulk_upsert import PreferBulkUpsert
246
251
  from sarj_python_lint.rules.prefer_direct_import_module import PreferDirectImportModule
247
252
  from sarj_python_lint.rules.prefer_discriminated_model_union import PreferDiscriminatedModelUnion
248
253
  from sarj_python_lint.rules.prefer_grouped_settings import PreferGroupedSettings
254
+ from sarj_python_lint.rules.prefer_optional_composite import PreferOptionalComposite
249
255
  from sarj_python_lint.rules.prefer_precise_object_contract import PreferPreciseObjectContract
250
256
  from sarj_python_lint.rules.prefer_psycopg_executemany import PreferPsycopgExecutemany
251
257
  from sarj_python_lint.rules.prefer_pydantic_model_assertion import PreferPydanticModelAssertion
@@ -259,11 +265,23 @@ from sarj_python_lint.rules.redundant_test_constructor_forwarder import Redundan
259
265
  from sarj_python_lint.rules.repeated_unimplemented_test_contract import RepeatedUnimplementedTestContract
260
266
  from sarj_python_lint.rules.require_exp_on_jwt_decode import RequireExpOnJwtDecode
261
267
  from sarj_python_lint.rules.store_field_update_reuses_upsert import StoreFieldUpdateReusesUpsert
268
+ from sarj_python_lint.rules.store_insert_requires_explicit_conflict_policy import (
269
+ StoreInsertRequiresExplicitConflictPolicy,
270
+ )
271
+ from sarj_python_lint.rules.store_mutation_command_fanout import StoreMutationCommandFanout
262
272
  from sarj_python_lint.rules.test_without_explicit_oracle import NoExplicitTestOracle
263
273
 
264
274
 
265
275
  REGISTRY: Mapping[str, type[Rule]] = MappingProxyType(
266
276
  {
277
+ NoDiscardedTaggedOutcome.id: NoDiscardedTaggedOutcome,
278
+ StoreInsertRequiresExplicitConflictPolicy.id: StoreInsertRequiresExplicitConflictPolicy,
279
+ StoreMutationCommandFanout.id: StoreMutationCommandFanout,
280
+ PreferApplicationModelJoins.id: PreferApplicationModelJoins,
281
+ NoRecordingStore.id: NoRecordingStore,
282
+ NoRedundantSqlAlias.id: NoRedundantSqlAlias,
283
+ NoCustomControlFlowException.id: NoCustomControlFlowException,
284
+ PreferOptionalComposite.id: PreferOptionalComposite,
267
285
  AsyncSubprocessCancellationCleanup.id: AsyncSubprocessCancellationCleanup,
268
286
  CancelOwnedTaskWithoutJoin.id: CancelOwnedTaskWithoutJoin,
269
287
  AsyncExecutorShutdown.id: AsyncExecutorShutdown,
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ from dataclasses import dataclass
5
+ import re
6
+ from typing import TYPE_CHECKING
7
+
8
+ from sqlglot import exp
9
+
10
+ from sarj_python_lint.rules._sql import sql_string_value
11
+ from sarj_python_lint.rules._store_writes import Function, methods
12
+ from sarj_python_lint.rules.no_psycopg_execution_outside_injected_owner import psycopg_execution_calls
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import Iterator
17
+
18
+ from sarj_python_lint._file_context import PythonFileContext
19
+
20
+
21
+ _HOLE = "__sarj_store_sql_hole__"
22
+ _FORMAT_HOLE = re.compile(r"(?<!\{)\{(?:[A-Za-z_]\w*|\d*)\}(?!\})")
23
+ _MAX_RECOVERY_DEPTH = 8
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class StoreQuery:
28
+ node: ast.expr
29
+ owner: ast.ClassDef | None
30
+ method: Function | None
31
+ sql: str
32
+ statement: exp.Expr
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class QueryOwner:
37
+ owner: ast.ClassDef | None
38
+ method: Function | None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class _MethodWrites:
43
+ qualified: frozenset[tuple[str, str]]
44
+ instances: frozenset[tuple[ast.ClassDef, str]]
45
+
46
+
47
+ def store_queries(context: PythonFileContext) -> Iterator[StoreQuery]:
48
+ if context.store_queries is None:
49
+ context.store_queries = tuple(recover_store_queries(context))
50
+ return iter(context.store_queries)
51
+
52
+
53
+ def recover_store_queries(context: PythonFileContext) -> Iterator[StoreQuery]:
54
+ calls = psycopg_execution_calls(context, include_injected=True, include_probes=False)
55
+ wrappers = _query_wrappers(calls, context)
56
+ for call in calls:
57
+ argument = _execution_argument(call)
58
+ if argument is not None:
59
+ yield from _parsed_query(argument, call, context)
60
+ for call in context.nodes(ast.Call):
61
+ yield from _forwarded_query(call, wrappers, context)
62
+
63
+
64
+ def _forwarded_query(
65
+ call: ast.Call, wrappers: dict[tuple[ast.ClassDef, str], tuple[int, str]], context: PythonFileContext
66
+ ) -> Iterator[StoreQuery]:
67
+ if (
68
+ not isinstance(call.func, ast.Attribute)
69
+ or not isinstance(call.func.value, ast.Name)
70
+ or call.func.value.id != "self"
71
+ ):
72
+ return
73
+ scope = query_owner(call, context)
74
+ wrapper = wrappers.get((scope.owner, call.func.attr)) if scope.owner is not None else None
75
+ if wrapper is None:
76
+ return
77
+ positional, parameter = wrapper
78
+ expression = (
79
+ call.args[positional]
80
+ if positional < len(call.args)
81
+ else next((keyword.value for keyword in call.keywords if keyword.arg == parameter), None)
82
+ )
83
+ if expression is not None:
84
+ yield from _parsed_query(expression, call, context)
85
+
86
+
87
+ def _execution_argument(call: ast.Call) -> ast.expr | None:
88
+ return call.args[0] if call.args else next((item.value for item in call.keywords if item.arg == "query"), None)
89
+
90
+
91
+ def _query_wrappers(
92
+ calls: list[ast.Call], context: PythonFileContext
93
+ ) -> dict[tuple[ast.ClassDef, str], tuple[int, str]]:
94
+ wrappers: dict[tuple[ast.ClassDef, str], tuple[int, str]] = {}
95
+ if not calls:
96
+ return wrappers
97
+ writes = _method_writes(context)
98
+ owner_methods: dict[ast.ClassDef, dict[str, Function]] = {}
99
+ for call in calls:
100
+ scope = query_owner(call, context)
101
+ owner, method = scope.owner, scope.method
102
+ argument = _execution_argument(call)
103
+ if owner is None or method is None or not isinstance(argument, ast.Name):
104
+ continue
105
+ if owner not in owner_methods:
106
+ owner_methods[owner] = methods(owner)
107
+ if (
108
+ owner_methods[owner].get(method.name) is not method
109
+ or (owner.name, method.name) in writes.qualified
110
+ or (owner, method.name) in writes.instances
111
+ ):
112
+ continue
113
+ parameters = [*method.args.posonlyargs, *method.args.args]
114
+ if not parameters or parameters[0].arg != "self":
115
+ continue
116
+ positions = {value.arg: index for index, value in enumerate(parameters[1:])}
117
+ position = positions.get(argument.id)
118
+ binding = context.static_resource_provenance.runtime_binding(argument.id, argument)
119
+ if position is not None and binding is parameters[position + 1]:
120
+ wrappers[owner, method.name] = (position, argument.id)
121
+ return wrappers
122
+
123
+
124
+ def _method_writes(context: PythonFileContext) -> _MethodWrites:
125
+ qualified: set[tuple[str, str]] = set()
126
+ instances: set[tuple[ast.ClassDef, str]] = set()
127
+ for node in context.nodes(ast.Attribute):
128
+ if not isinstance(node.ctx, ast.Store | ast.Del) or not isinstance(node.value, ast.Name):
129
+ continue
130
+ qualified.add((node.value.id, node.attr))
131
+ if node.value.id == "self":
132
+ owner = query_owner(node, context).owner
133
+ if owner is not None:
134
+ instances.add((owner, node.attr))
135
+ return _MethodWrites(frozenset(qualified), frozenset(instances))
136
+
137
+
138
+ def query_owner(node: ast.AST, context: PythonFileContext) -> QueryOwner:
139
+ current = context.parents.get(node)
140
+ while current is not None and not isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef | ast.Lambda):
141
+ current = context.parents.get(current)
142
+ if not isinstance(current, ast.FunctionDef | ast.AsyncFunctionDef):
143
+ return QueryOwner(None, None)
144
+ parent = context.parents.get(current)
145
+ return QueryOwner(parent if isinstance(parent, ast.ClassDef) else None, current)
146
+
147
+
148
+ def _parsed_query(expression: ast.expr, call: ast.Call, context: PythonFileContext) -> Iterator[StoreQuery]:
149
+ text = _query_text(expression, context, seen=frozenset())
150
+ if text is None:
151
+ return
152
+ from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- malformed input abstains.
153
+
154
+ from sarj_python_lint.rules._postgres import parse_postgres # ruff: ignore[import-outside-top-level] -- avoid private-source unsupported SQL logs.
155
+
156
+ try:
157
+ statements = parse_postgres(_FORMAT_HOLE.sub(_HOLE, text))
158
+ except SqlglotError, RecursionError:
159
+ return
160
+ scope = query_owner(call, context)
161
+ for statement in statements:
162
+ if statement is not None:
163
+ yield StoreQuery(_query_origin(expression, context), scope.owner, scope.method, text, statement)
164
+
165
+
166
+ def _query_origin(expression: ast.expr, context: PythonFileContext) -> ast.expr:
167
+ seen: set[int] = set()
168
+ while id(expression) not in seen and len(seen) < _MAX_RECOVERY_DEPTH:
169
+ seen.add(id(expression))
170
+ if isinstance(expression, ast.Name):
171
+ value = _bound_value(expression, context)
172
+ if value is not None:
173
+ expression = value
174
+ continue
175
+ if (
176
+ isinstance(expression, ast.Call)
177
+ and isinstance(expression.func, ast.Attribute)
178
+ and expression.func.attr == "format"
179
+ ):
180
+ expression = expression.func.value
181
+ continue
182
+ if (
183
+ isinstance(expression, ast.Call)
184
+ and expression.args
185
+ and context.static_resource_provenance.runtime_import_name(expression.func) == "psycopg.sql.SQL"
186
+ ):
187
+ expression = expression.args[0]
188
+ continue
189
+ break
190
+ return expression
191
+
192
+
193
+ def relation_key(node: exp.Expr | None) -> tuple[str, ...] | None:
194
+ from sqlglot import exp # ruff: ignore[import-outside-top-level] -- native table identity for parsed queries.
195
+
196
+ if not isinstance(node, exp.Table) or not isinstance(sql_child(node, "this"), exp.Identifier):
197
+ return None
198
+ parts = (sql_child(node, "catalog"), sql_child(node, "db"), sql_child(node, "this"))
199
+ identifiers = [part for part in parts if part is not None]
200
+ if not all(isinstance(part, exp.Identifier) for part in identifiers):
201
+ return None
202
+ if any(not part.args.get("quoted") and not part.name.isascii() for part in identifiers):
203
+ return None
204
+ names = tuple(part.name if part.args.get("quoted") else part.name.lower() for part in identifiers)
205
+ return None if any(_HOLE in name for name in names) else names
206
+
207
+
208
+ def sql_child(node: exp.Expr, key: str) -> exp.Expr | None:
209
+ return next((child for child in node.iter_expressions() if child.arg_key == key), None)
210
+
211
+
212
+ def sql_children(node: exp.Expr, key: str) -> tuple[exp.Expr, ...]:
213
+ return tuple(child for child in node.iter_expressions() if child.arg_key == key)
214
+
215
+
216
+ def _query_text(expression: ast.expr, context: PythonFileContext, *, seen: frozenset[int]) -> str | None:
217
+ if id(expression) in seen or len(seen) >= _MAX_RECOVERY_DEPTH:
218
+ return None
219
+ seen |= {id(expression)}
220
+ literal = sql_string_value(expression, interpolation_placeholder=_HOLE)
221
+ if literal is not None:
222
+ return literal
223
+ match expression:
224
+ case ast.Name():
225
+ value = _bound_value(expression, context)
226
+ return _query_text(value, context, seen=seen) if value is not None else None
227
+ case ast.Call(func=ast.Attribute(attr="format", value=value)):
228
+ return _query_text(value, context, seen=seen)
229
+ case ast.Call(func=function, args=[argument], keywords=[]):
230
+ if context.static_resource_provenance.runtime_import_name(function) == "psycopg.sql.SQL":
231
+ return _query_text(argument, context, seen=seen)
232
+ case ast.BinOp(left=left, op=ast.Add(), right=right):
233
+ left_sql = _query_text(left, context, seen=seen)
234
+ right_sql = _query_text(right, context, seen=seen)
235
+ if left_sql is not None or right_sql is not None:
236
+ return (left_sql or _HOLE) + (right_sql or _HOLE)
237
+ case _:
238
+ pass
239
+ return None
240
+
241
+
242
+ def _bound_value(expression: ast.Name, context: PythonFileContext) -> ast.expr | None:
243
+ provenance = context.static_resource_provenance
244
+ binding = provenance.runtime_binding(expression.id, expression)
245
+ return provenance.assignment_value(binding, expression) if isinstance(binding, ast.Name) else None
@@ -20,6 +20,7 @@ from sarj_python_lint.rules._ast_index import nodes, walk
20
20
  from sarj_python_lint.rules._ast_names import unqualified_name as _call_name
21
21
  from sarj_python_lint.rules._paths import is_test_path
22
22
  from sarj_python_lint.rules._sql import is_store_module, sql_string_value, strip_sql_noise
23
+ from sarj_python_lint.rules._store_sql import relation_key, store_queries
23
24
  from sarj_python_lint.rules.no_analytical_aggregation_in_postgres_store import analytical_signal
24
25
 
25
26
 
@@ -65,6 +66,7 @@ _COMBINED_JOIN_LIMIT = 3
65
66
  _COMBINED_STAGE_LIMIT = 4
66
67
  _WIDE_PROJECTION_LIMIT = 25
67
68
  _WIDE_PROJECTION_JOIN_LIMIT = 2
69
+ _WIDE_SELF_JOIN_PROJECTION_LIMIT = 16
68
70
 
69
71
 
70
72
  class _ConstructorImport(NamedTuple):
@@ -175,14 +177,13 @@ def _recoverable_query_root(node: ast.expr, constructors: frozenset[str]) -> ast
175
177
 
176
178
  def _qualified_name(node: ast.expr) -> str:
177
179
  match node:
178
- case ast.Name():
179
- return node.id
180
- case ast.Attribute():
181
- owner = _qualified_name(node.value)
182
- return f"{owner}.{node.attr}" if owner else node.attr
180
+ case ast.Name(id=name):
181
+ return name
182
+ case ast.Attribute(value=value, attr=attribute):
183
+ owner = _qualified_name(value)
184
+ return f"{owner}.{attribute}" if owner else attribute
183
185
  case _:
184
- pass
185
- return ""
186
+ return ""
186
187
 
187
188
 
188
189
  def _visible_constructors(
@@ -259,9 +260,9 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
259
260
  autofix=AutofixPolicy.NONE,
260
261
  limitations=(
261
262
  "Only statically recoverable SQL passed to execute, executemany, fetch, fetchrow, fetchval, or prepare in non-test modules with explicit PostgreSQL imports is analyzed, regardless of module filename.",
262
- "Execution receivers must use a conventional database name; custom wrappers and dynamically obtained receivers abstain.",
263
- "One direct, unambiguous simple-name binding in the same lexical scope is followed; standalone constants, branches, aliases, attributes, containers, wildcard imports, and cross-scope flow abstain.",
264
- "Review covers derived relations in every SELECT (including LATERAL, CTEs, predicates and writes), four explicit or comma joins, six query stages, three joins plus four stages, or 25 projections plus two joins.",
263
+ "Execution receivers use a conventional database name or import-proven Psycopg ownership. Native execution additionally follows stable module/local SQL bindings and direct same-class query-forwarding wrappers; unknown receivers abstain.",
264
+ "Conventional receivers follow one direct, unambiguous same-scope binding. Native Psycopg recovery may follow bounded stable lexical bindings, including module constants. Unexecuted constants, branch-local assignments, mutable attributes, containers and unknown dynamic composition abstain.",
265
+ "Review covers derived relations in every SELECT (including LATERAL, CTEs, predicates and writes), four explicit or comma joins, six query stages, three joins plus four stages, or 25 projections plus two joins. A self join over the same explicit table also merits review at 16 projected expressions.",
265
266
  "Derived-relation review requires grouping, HAVING or windowing combined with joins or nested reads; expression density is not a separate trigger.",
266
267
  "Projection width alone is accepted, and UNION branches are measured independently; execution plans and production cardinality remain authoritative.",
267
268
  "ClickHouse and BigQuery syntax is excluded per query, including in mixed-backend modules.",
@@ -352,42 +353,57 @@ class ComplexPostgresQueryRequiresArchitectureReview(Rule):
352
353
  if not _is_postgres_module(tree, node_index=context.node_index):
353
354
  return []
354
355
 
355
- docstrings = context.docstring_node_ids
356
- parents = context.parents
357
- query_roots = _query_roots(tree, parents, node_index=context.node_index)
358
- diagnostics: list[Diagnostic] = []
359
- consumed: set[int] = set()
360
- for node in context.nodes(ast.Constant, ast.BinOp, ast.JoinedStr):
361
- if id(node) in consumed or id(node) in docstrings or not _is_query_context(node, parents, query_roots):
362
- continue
363
- text_value = sql_string_value(node, interpolation_placeholder=_SQL_HOLE)
364
- if text_value is None:
365
- continue
366
- if isinstance(node, (ast.BinOp, ast.JoinedStr)):
367
- consumed.update(id(child) for child in walk(node))
368
-
369
- signal = _query_architecture_signal(text_value, path)
356
+ diagnostics = _conventional_findings(context, self.code)
357
+ for query in store_queries(context):
358
+ signal = _query_architecture_signal(query.sql, path)
370
359
  if signal is None:
371
360
  continue
372
- guidance = _query_review_guidance(text_value)
373
- diagnostics.append(
374
- Diagnostic(
375
- path=path,
376
- line=node.lineno,
377
- col=node.col_offset + 1,
378
- code=self.code,
379
- severity=Severity.WARNING,
380
- message=(
381
- f"Complex PostgreSQL query: {signal}. Architecture review required;{guidance} "
382
- "Prefer ClickHouse for reporting, historical analysis, and broad aggregations. "
383
- "For transactional work, simplify the query or document why PostgreSQL is required, "
384
- "with query bounds and EXPLAIN evidence. Do not add reporting-specific indexes without "
385
- "evaluating ClickHouse. Syntax alone does not establish runtime cost or datastore placement."
386
- ),
387
- )
361
+ node = query.node
362
+ diagnostics.append(_native_diagnostic(context, node, signal, self.code, query.sql))
363
+ unique = {(finding.line, finding.col): finding for finding in diagnostics}
364
+ return sorted(unique.values(), key=lambda finding: (finding.line, finding.col))
365
+
366
+
367
+ def _conventional_findings(context: PythonFileContext, code: str) -> list[Diagnostic]:
368
+ path = context.path
369
+ docstrings = context.docstring_node_ids
370
+ parents = context.parents
371
+ tree = context.tree
372
+ if tree is None:
373
+ return []
374
+ query_roots = _query_roots(tree, parents, node_index=context.node_index)
375
+ diagnostics: list[Diagnostic] = []
376
+ consumed: set[int] = set()
377
+ for node in context.nodes(ast.Constant, ast.BinOp, ast.JoinedStr):
378
+ if id(node) in consumed or id(node) in docstrings or not _is_query_context(node, parents, query_roots):
379
+ continue
380
+ text_value = sql_string_value(node, interpolation_placeholder=_SQL_HOLE)
381
+ if text_value is None:
382
+ continue
383
+ if isinstance(node, (ast.BinOp, ast.JoinedStr)):
384
+ consumed.update(id(child) for child in walk(node))
385
+
386
+ signal = _query_architecture_signal(text_value, path)
387
+ if signal is None:
388
+ continue
389
+ guidance = _query_review_guidance(text_value)
390
+ diagnostics.append(
391
+ Diagnostic(
392
+ path=path,
393
+ line=node.lineno,
394
+ col=node.col_offset + 1,
395
+ code=code,
396
+ severity=Severity.WARNING,
397
+ message=(
398
+ f"Complex PostgreSQL query: {signal}. Architecture review required;{guidance} "
399
+ "Prefer ClickHouse for reporting, historical analysis, and broad aggregations. "
400
+ "For transactional work, simplify the query or document why PostgreSQL is required, "
401
+ "with query bounds and EXPLAIN evidence. Do not add reporting-specific indexes without "
402
+ "evaluating ClickHouse. Syntax alone does not establish runtime cost or datastore placement."
403
+ ),
388
404
  )
389
- diagnostics.sort(key=lambda diagnostic: (diagnostic.line, diagnostic.col))
390
- return diagnostics
405
+ )
406
+ return diagnostics
391
407
 
392
408
 
393
409
  def _count_query_bindings(
@@ -473,12 +489,13 @@ def _query_architecture_signal(text_value: str, path: Path) -> str | None:
473
489
 
474
490
 
475
491
  def _parse_signal(sql: str) -> str | None:
476
- import sqlglot # ruff: ignore[import-outside-top-level] -- parse only SQL that passes cheap ownership and shape gates
477
492
  from sqlglot.errors import SqlglotError # ruff: ignore[import-outside-top-level] -- paired with lazy parser import
478
493
 
494
+ from sarj_python_lint.rules._postgres import parse_postgres # ruff: ignore[import-outside-top-level] -- unsupported SQL must not log private source.
495
+
479
496
  normalized = _COMPOSABLE_HOLE.sub(_SQL_HOLE, sql)
480
497
  try:
481
- for statement in sqlglot.parse(normalized, read="postgres"):
498
+ for statement in parse_postgres(normalized):
482
499
  if statement is not None and (signal := _architecture_signal(statement)) is not None:
483
500
  return signal
484
501
  except SqlglotError:
@@ -502,14 +519,45 @@ def _architecture_signal(statement: exp.Expr) -> str | None:
502
519
  return f"Query has {nested_queries} CTE/subquery stages"
503
520
  if maximum_joins >= _COMBINED_JOIN_LIMIT and nested_queries >= _COMBINED_STAGE_LIMIT:
504
521
  return f"Query combines {maximum_joins} JOINs with {nested_queries} CTE/subquery stages"
522
+ return _projection_width_signal(statement)
523
+
524
+
525
+ def _projection_width_signal(statement: exp.Expr) -> str | None:
526
+ from sqlglot import exp # ruff: ignore[import-outside-top-level] -- only parsed projection shapes need types.
527
+
505
528
  for select in statement.find_all(exp.Select):
506
529
  projections = tuple(_expression_args(select, "expressions"))
507
530
  joins = tuple(join for join in _expression_args(select, "joins") if isinstance(join, exp.Join))
508
531
  if len(projections) >= _WIDE_PROJECTION_LIMIT and len(joins) >= _WIDE_PROJECTION_JOIN_LIMIT:
509
532
  return f"Joined query block has {len(projections)} projected expressions"
533
+ if len(projections) >= _WIDE_SELF_JOIN_PROJECTION_LIMIT and _self_join(select, joins):
534
+ return f"Self-joined query block has {len(projections)} projected expressions"
510
535
  return None
511
536
 
512
537
 
538
+ def _native_diagnostic(context: PythonFileContext, node: ast.expr, signal: str, code: str, sql: str) -> Diagnostic:
539
+ return Diagnostic(
540
+ context.path,
541
+ node.lineno,
542
+ node.col_offset + 1,
543
+ code,
544
+ f"Complex PostgreSQL query: {signal}. Architecture review required;{_query_review_guidance(sql)} "
545
+ "Prefer ClickHouse for reporting, historical analysis, and broad aggregations. "
546
+ "For transactional work, simplify the query or document why PostgreSQL is required, "
547
+ "with query bounds and EXPLAIN evidence. Do not add reporting-specific indexes without "
548
+ "evaluating ClickHouse. Syntax alone does not establish runtime cost or datastore placement.",
549
+ severity=Severity.WARNING,
550
+ )
551
+
552
+
553
+ def _self_join(select: exp.Select, joins: tuple[exp.Join, ...]) -> bool:
554
+ from sqlglot import exp # ruff: ignore[import-outside-top-level] -- compare only literal relation identities.
555
+
556
+ from_clause = _expression_arg(select, "from_")
557
+ root = relation_key(_expression_arg(from_clause, "this")) if isinstance(from_clause, exp.From) else None
558
+ return root is not None and any(relation_key(_expression_arg(join, "this")) == root for join in joins)
559
+
560
+
513
561
  def _derived_architecture_signal(statement: exp.Expr) -> str | None:
514
562
  from sqlglot import exp # ruff: ignore[import-outside-top-level] -- keep parser startup off unrelated lint runs
515
563