Skip to content

The sentinel sweep derives what it selects but not what it excludes, so wrote() is out by accident #938

Description

@jdatcmd

_comparisons() in test_failed_query_sentinel.py decides which of Expect's public methods must refuse a QUERY_ERROR sentinel. It selects on the first two parameter names: got plus want or floor.

That derivation is why the sweep is worth having — an assertion added later is covered by it instead of being the next hole. But the exclusion side is not derived at all, and @OffgridwithJD raised the case that shows it:

wrote(cur, want, name) sits outside the sweep because its first parameter is a cursor rather than a value. That is the right answer, but it is right by accident — a future assertion whose first parameter is simply not called got would be excluded just as silently and for no good reason.

Why a declared exclusion list is not the fix

The obvious remedy is a list of excluded method names with a reason each. This layer argues against exactly that shape, repeatedly and with evidence:

  • _WRITE_TAGS is a list, and four statement shapes escape it (a data-modifying CTE, a DO block, a SELECT of an inserting function, a CALL) — recorded as a residual precisely because a list cannot see what it does not name.
  • test_harness_deps.py exists because a hand-maintained NO_CLUSTER drifts, and the fix there was to derive membership and assert the declaration against it.
  • selftest 400's SKIP sweep and 080's heredoc exemption were both rewritten from filename lists to derived properties for the same reason.

A list of exclusions is a convention. This directory only counts a mode refused when a mechanism refuses it.

The shape to build instead

Derive the exclusion, then assert the partition:

  1. Every public method of Expect lands in exactly one bucket — selected, or excluded — with no third state and no method matching neither rule.
  2. Each exclusion is derived from a property of the signature, not from a name. wrote is excluded because its left operand is a cursor rather than a caller-supplied value; cannot_run, run_failed and outcomes because they take a result object; plan_marker and plan_node because they take a plan. Whatever the rules turn out to be, each must be checkable from the signature.
  3. The reason travels with the exclusion, so a reader sees why a method is out rather than inferring it from a missing entry.
  4. An arm prints inputs == selected + excluded and fails when a method matches neither rule — which is the case that must not be silent, and is what happens today when a parameter is not called got.

The current enumeration already reconciles — inputs 15 == selected 8 + not selected 7 — but "not selected" is a residue rather than a decision, and nothing fails when a new method lands in it for the wrong reason.

Acceptance

  • Adding a method to Expect whose left operand is a caller-supplied value, with a first parameter not named got, must fail an arm rather than silently leave the sweep.
  • Adding a method that is legitimately excluded must require a stated reason, and the arm must pass once it has one.
  • Removal proof for both: name the mutation, apply it, show which arm reddens.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01Unuuvh3fRR67SceiGpfeeK

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions