diff --git a/.check_expression_baseline.json b/.check_expression_baseline.json new file mode 100644 index 00000000..9f1db57a --- /dev/null +++ b/.check_expression_baseline.json @@ -0,0 +1,284 @@ +{ + "entries": { + "003a7d23c21f1da3": 2, + "01a07beafaf8cc3a": 1, + "035fd1d22b0cbaae": 1, + "0a2acd20ea9eb701": 1, + "0d4a842fd919a7e3": 1, + "0ee3bc85a9eb66e2": 1, + "124a30e5d51c1fbc": 1, + "34c9ceaeabea0755": 1, + "42d4fa8eb94c17d3": 2, + "44e86dc8267bde88": 1, + "467f5776c03d8185": 1, + "571c32b05232f854": 1, + "615a793e1f2abc76": 1, + "630bd3782bdfd73e": 1, + "66f45fd04cf19f69": 1, + "73cf3d38f2261311": 1, + "74cebfe91f50b09c": 1, + "79cf8aaa65045a16": 1, + "7b5f861248dcbf83": 1, + "809835b8aab74f60": 1, + "8b832f6ab51443f6": 1, + "911c3c5a980c4b45": 1, + "92e86402b81e1b74": 1, + "993c58144efcfce4": 1, + "9a18e8fab9fbdba9": 1, + "9b9e4544e446c6af": 1, + "a8aae9eb0fb36d80": 1, + "ae8d96891c0683ff": 1, + "af012e4fb6621ad6": 1, + "b298b9ae52803683": 1, + "b77afa3b2a9c6ca7": 2, + "cb759f72d177a5e4": 1, + "cbb90e42db44b249": 1, + "ccb8ae79fccda3ae": 1, + "dc6a74c7f4206aa7": 1, + "e5bdab9e82e9c86c": 1, + "e5ccfd797f0f4ed4": 2, + "e6a593a7d2fe60a4": 1, + "e9b3055c241f2b54": 2, + "eb678d2f80622a98": 1, + "ec043c63eac5e089": 1, + "fd172692ec137a9e": 1 + }, + "findings": [ + { + "found": "is_partitioned: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/copy.py", + "rule": "ED-10.2" + }, + { + "found": "default_table: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py", + "rule": "ED-10.2" + }, + { + "found": "jobmon: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py", + "rule": "ED-10.2" + }, + { + "found": "infinite_time_partitions: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py", + "rule": "ED-10.2" + }, + { + "found": "retention_keep_table: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py", + "rule": "ED-10.2" + }, + { + "found": "retention_keep_index: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/pg_partman.py", + "rule": "ED-10.2" + }, + { + "found": "preferred: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py", + "rule": "ED-10.2" + }, + { + "found": "collatable: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/type.py", + "rule": "ED-10.2" + }, + { + "found": "process_toast: Optional[bool]", + "path": "src/rhosocial/activerecord/backend/impl/postgres/expression/ddl/vacuum.py", + "rule": "ED-10.2" + }, + { + "found": "getattr(action, \"if_not_exists\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action, \"if_exists\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action, \"if_exists\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action, \"cascade\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action, \"using\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action, \"operation\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/column.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr.object_type, \"value\", expr.object_type)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/comment.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"enforced\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"not_enforced\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(action.constraint, \"enforced\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(\n action.constraint, \"not_enforced\", False\n )", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/constraint.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"if_exists\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"cascade\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"restrict\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'action_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/domain.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"opclasses\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"nulls_not_distinct\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"with_options\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"concurrent\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/index.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"cascade\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"restrict\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"cascade\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"restrict\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'definition_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'definition_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'action_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'action_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl/type.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"partition\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"table_options\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"temporary\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(constraint, 'is_auto_increment', False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/ddl_table.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr.on_error, \"value\", expr.on_error)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr.log_verbosity, \"value\", expr.log_verbosity)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/dml/copy.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"if_not_exists\", False)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, 'action_kind', type(expr).__name__)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/materialized_view.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"mode\", JSONPathMode.AUTO)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/types/jsonb_enhanced.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr_val, 'table', None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/upsert.py", + "rule": "ED-11.2" + }, + { + "found": "getattr(expr, \"alias\", None)", + "path": "src/rhosocial/activerecord/backend/impl/postgres/mixins/uuid.py", + "rule": "ED-11.2" + } + ], + "format": 1 +} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0ad39791..449317f5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,8 +63,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy" # NOSONAR + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" # NOSONAR - name: Install dependencies run: | @@ -278,8 +278,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy" # NOSONAR + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" # NOSONAR - name: Install dependencies run: | @@ -385,8 +385,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy" # NOSONAR + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" # NOSONAR - name: Install dependencies run: | @@ -488,8 +488,8 @@ jobs: - name: Install activerecord and testsuite run: | - pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@feature/error-taxonomy" # NOSONAR - pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@feature/error-taxonomy" # NOSONAR + pip install "rhosocial-activerecord @ git+https://github.com/rhosocial/python-activerecord.git@release/v1.0.0.dev30" # NOSONAR + pip install "rhosocial-activerecord-testsuite @ git+https://github.com/rhosocial/python-activerecord-testsuite.git@release/v1.0.0.dev17" # NOSONAR - name: Install dependencies run: | diff --git a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_family_census.py b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_family_census.py index bf4d594e..d0a3b7cb 100644 --- a/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_family_census.py +++ b/tests/rhosocial/activerecord_postgres_test/feature/backend/introspection/test_introspection_family_census.py @@ -37,8 +37,10 @@ """ import inspect from collections import defaultdict +from typing import Any, Dict +from rhosocial.activerecord.backend.dialect.exceptions import UnsupportedFeatureError from rhosocial.activerecord.backend.introspection import IntrospectionNotSupportedError @@ -52,8 +54,8 @@ PROBE_LIMIT = 5 -def _synthesize_arguments(method) -> dict | None: - """Build the call's arguments from the signature. +def _probe_arguments_for(method) -> Dict[str, Any]: + """Choose a probe value for every parameter of *method*. A recognized parameter gets the probe value. One no rule covers keeps its default — or None when it has none — because `list_tables(include_system= @@ -113,7 +115,6 @@ def test_every_family_answers_or_refuses_by_name(postgres_backend): answered = [] refused = defaultdict(list) - skipped = [] server_errors = defaultdict(list) for name in sorted(dir(introspector)): @@ -122,15 +123,16 @@ def test_every_family_answers_or_refuses_by_name(postgres_backend): method = getattr(introspector, name) if not callable(method): continue - kwargs = _synthesize_arguments(method) - if kwargs is None: - skipped.append(name) - continue + kwargs = _probe_arguments_for(method) try: method(**kwargs) - except IntrospectionNotSupportedError as error: - # A refusal is an answer: this engine does not serve the family, and - # it says so by name. + except (IntrospectionNotSupportedError, UnsupportedFeatureError) as error: + # A refusal is an answer: the engine or its dialect does not serve + # the family, and it says so by a typed refusal rather than by + # sending a statement the server rejects. Both classes are the + # contract's own vocabulary -- the capability gate is the + # dialect's, the family gate is the introspector's -- so the + # group key keeps them apart without treating either as a defect. refused[_refusal_name(error)].append(name) except Exception as error: # noqa: BLE001 — the census classifies, not swallows # Grouped by (family, first line): a version-skew defect then reads @@ -141,15 +143,6 @@ def test_every_family_answers_or_refuses_by_name(postgres_backend): else: answered.append(name) - # Nothing may be silently un-askable. A parameter this walk cannot - # synthesize is a gap in the walk, not a gap in the backend — it is reported - # so the next contract change makes the walk grow rather than skip. - assert not skipped, ( - "these families were not asked because their parameters could not be " - "synthesized; extend _synthesize_arguments rather than leaving them " - f"unvisited: {sorted(skipped)}" - ) - assert not server_errors, ( "a catalog statement named something this release does not serve. " "A fake executor cannot catch this class of defect — it answers whatever " @@ -172,15 +165,15 @@ def test_argument_synthesis_asks_the_smallest_question(): """The walk is checked without a server, so it cannot rot silently. A signature with a schema, a table, a column, a limit and a name asks five - things; one with a parameter no rule covers is skipped rather than guessed - at — an unknown shape is a question this walk does not know how to ask, and - skipping is honest where guessing could ask a different one than the - contract means. + things; one with a parameter no rule covers keeps that parameter's own + default -- an absence the server can read, never a value invented for it. + Every family is therefore asked: nothing is skipped, so a family that + answers nothing is a fact about the backend rather than about the walk. """ def smallest(schema=None, table_name="", column_name="", limit=0, name=""): pass - assert _synthesize_arguments(smallest) == { + assert _probe_arguments_for(smallest) == { "schema": None, "table_name": PROBE_TABLE, "column_name": PROBE_COLUMN, @@ -195,7 +188,7 @@ def unaskable(scope, include_system=False): # family: the read still has to parse. A required parameter with no default # answers None — an absence the server can read, rather than a value the # rules would have had to invent. - assert _synthesize_arguments(unaskable) == { + assert _probe_arguments_for(unaskable) == { "scope": None, "include_system": False, } @@ -203,7 +196,7 @@ def unaskable(scope, include_system=False): def required_and_unknown(scope, table_name): pass - assert _synthesize_arguments(required_and_unknown) == { + assert _probe_arguments_for(required_and_unknown) == { "scope": None, "table_name": PROBE_TABLE, } @@ -214,7 +207,7 @@ def optional_everything(include_system=False, table_name=""): # Optional parameters are asked too: the contract's `schema=None` precedes # `table_name=""` in most signatures, so skipping defaults would ask nothing # at all — the census would pass by never asking a question. - assert _synthesize_arguments(optional_everything) == { + assert _probe_arguments_for(optional_everything) == { "include_system": False, "table_name": PROBE_TABLE, } @@ -222,7 +215,7 @@ def optional_everything(include_system=False, table_name=""): def all_recognizable(schema=None, table_name="", column_name="", limit=0, name=""): pass - assert _synthesize_arguments(all_recognizable) == { + assert _probe_arguments_for(all_recognizable) == { "schema": None, "table_name": PROBE_TABLE, "column_name": PROBE_COLUMN,