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.
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/PKG-INFO +1 -1
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/pyproject.toml +1 -1
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix.py +2 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_file_context.py +2 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/_postgres.py +17 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_registry.py +18 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/_store_sql.py +245 -0
- {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
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_custom_control_flow_exception.py +237 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py +469 -0
- {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
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_recording_store.py +276 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/no_redundant_sql_alias.py +164 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/prefer_application_model_joins.py +138 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_type_dispatch.py +125 -2
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/prefer_optional_composite.py +297 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py +105 -0
- sarj_python_lint-1.85.0/src/sarj_python_lint/rules/store_mutation_command_fanout.py +172 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/.gitignore +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/LICENSE +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/README.md +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/__init__.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/__main__.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_analysis_session.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_aliases.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_annotations.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_comments.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_declared_contracts.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_declared_tests.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_docs.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_enums.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_exhaustiveness.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_inert.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_parameters.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_repeated.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_security.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_sql_projection.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_autofix_tests.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_filesystem.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_fixes.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_parallel.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_python_target.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_ratchet_cli.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_secret_names.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_source.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/_version.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/application_logging.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/autofix.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/benchmark.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/cast_access.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/interpreter_argv.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/json_boundary.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/py.typed +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/ratchet.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rule_base.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/__init__.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_annotation_origins.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_annotation_semantics.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_index.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_names.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_ast_position.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_async_lifecycle.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_binding_inventory.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_closed_helpers.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_comment_narration.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_comments.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_contract_annotation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_copy_on_write.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_data_union_discriminator.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_declared_contracts.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_dict_key_views.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_docstrings.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_exports.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_fastapi.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_first_party.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_fixed_record.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_imports.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_inert_model_annotation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_inherited_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_local_alias_fix.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_local_source.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_logging.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_mapping_shape_callbacks.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_namespace_access.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_native_namespace_capability.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_native_object_contracts.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_nominal_project.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_numeric_proof.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_opaque_contracts.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_paths.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_project_index.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_prose_budget.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_pydantic_field_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_pytest.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_resolved_operation_signature.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_resource_provenance.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_route_shapes.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_runtime_bindings.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_service_http_callbacks.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_set_fields.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_aggregate_identity.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_explain.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_sql_policy_source.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_static_json_alias.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_store_writes.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_suppression_comments.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_assertions.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_composition.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_test_provenance.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_transactional_sql.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/_type_guard_symbols.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_executor_shutdown.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/docstring_args_restate_signature.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/docstring_returns_restate_signature.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/excessive_commentary.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/failed_probe_zero_threshold.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fakes_in_shared_location.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fastapi_class_router_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/iac_source_coupled_test.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/invalid_pydantic_field_default.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/mock_without_spec.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/named_record_at_boundaries.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_any_mapping_types.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_async_function_cache.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_blanket_suppression.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_broad_keyword_capture.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_comment_cruft.py +0 -0
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_contradictory_type_guard.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_copied_inherited_docstring.py +0 -0
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_database_programmability.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_defaulted_environment_setting.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_delete_statement.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_discarded_validation_result.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_dunder_all.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_fastapi_on_event.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_hasattr.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_injected_module_loader.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_input_model_mutation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_interpreter_source_arguments.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_loguru_exc_info.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_loguru_printf_format.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_mutating_container_transform.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_offset_pagination.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_partial_type_is.py +0 -0
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_redundant_literal_description.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_redundant_local_alias.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_repeated_string_literal.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_repeated_test_body.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_restated_comment.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_secret_in_log.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_select_star.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_tautological_expect.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_test_method_grafting.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_typed_doc_sections.py +0 -0
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unnecessary_docstring.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unused_copy_result.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_unused_value_marker.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/no_vague_suppression_description.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/over_mocked_test.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/phase_label_comment.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_bulk_upsert.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_class_row.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_collection_comprehension.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_direct_import_module.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_discriminated_model_union.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_fstring_over_concat.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_grouped_settings.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_immutable_module_constant.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_library_fake.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_assert_never.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_match_value_dispatch.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_module_level_constant.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_native_string_check.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_nominal_id_types.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_non_nullable_collection.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_or_pattern.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_precise_object_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_psycopg_executemany.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pydantic_json_value.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_regex_fullmatch.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_runtime_validation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_self_documenting_constant.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_self_type_annotation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_set_isdisjoint.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_str_enum.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_cli.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_mapping_get.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_typed_settings_field.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_regex_match.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/production_derived_test_cases.py +0 -0
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_class_docstring.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_docstring.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_module_docstring.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_static_call_cases.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_test_composition.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py +0 -0
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_contract_implementation.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_explicit_service_contract.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_injectable_retry_sleep.py +0 -0
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_port_for_service.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_precise_factory_signature.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/require_public_dependency_contract.py +0 -0
- {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
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/restated_test_docstring.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/stepdown.py +0 -0
- {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
- {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
- {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
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/subprocess_kill_without_reap.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/test_without_explicit_oracle.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/trailing_value_narration.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/typed_error_reasons.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/unused_mock_setup.py +0 -0
- {sarj_python_lint-1.84.0 → sarj_python_lint-1.85.0}/src/sarj_python_lint/rules/unused_test_factory_option.py +0 -0
- {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.
|
|
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/
|
|
@@ -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
|
|
180
|
-
case ast.Attribute():
|
|
181
|
-
owner = _qualified_name(
|
|
182
|
-
return f"{owner}.{
|
|
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
|
-
|
|
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
|
|
263
|
-
"
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
373
|
-
diagnostics.append(
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
|
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
|
|