Skip to content

test(mutation): kill sqlite_sql_translate.py's 20 surviving mutants (#265) - #285

Merged
cdeust merged 2 commits into
mainfrom
fix-sqlite-sql-translate-mutants-265
Jul 30, 2026
Merged

test(mutation): kill sqlite_sql_translate.py's 20 surviving mutants (#265)#285
cdeust merged 2 commits into
mainfrom
fix-sqlite-sql-translate-mutants-265

Conversation

@cdeust

@cdeust cdeust commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Issue #265 filed a scoped mutmut run (mutmut 3.x, coding-standards.md §12)
against mcp_server/infrastructure/sqlite_sql_translate.py's
_translate_sql/_returning_was_stripped — the SQL-dialect translation
module extracted from sqlite_compat.py in #260 — reporting 20 surviving
mutants. This PR closes it: every survivor is killed by a new test or
documented as a provable equivalent mutant.

Note on provenance: sqlite_sql_translate.py did not exist on main yet
when this work started (it is created by the still-open #266,
fix-sqlite-datetime-adapter-260). Rather than stack on that unmerged
branch — main advances fast in this repo from concurrent sessions, and a
stacked branch's own doc-count-bump commits kept conflicting on every
rebase — this PR cherry-picks #266's single substantive commit
(fix(sqlite): register explicit datetime adapter, close deprecation warning, byte-identical diff) directly onto current main, then adds the
#265 fix on top. When #266 merges first, expect a trivial merge: the
production-code files (sqlite_compat.py, sqlite_sql_translate.py,
test_wiki_pipeline_sqlite.py, test_sqlite_datetime_adapter_260.py) will
resolve automatically since both sides converge on identical final content;
only the shared doc-count files (.bestpractices.json, CLAUDE.md,
CONTRIBUTING.md, README.md, docs/ASSURANCE-CASE.md,
assets/badge-tests.svg) will show a real conflict — resolve by taking
either side's shape and re-running pytest --collect-only -q for the true
absolute, same as every other doc-count conflict in this repo's recent
history.

Root cause of the 20 survivors

Every one is the same shape: a mutant dropping (or re-spelling the case of)
the flags=re.IGNORECASE argument on one of the module's re.sub/re.search
calls. Every existing case-insensitivity fixture in
test_wiki_pipeline_sqlite.py happens to supply an input whose case already
matches the pattern's own literal spelling, so the flag's presence was never
observable by any existing assertion.

A fresh scoped run on this tree measured 19 of the 20 named ids still
surviving (mutmut_136 had flipped to killed between runs — confirmed
non-deterministic mutant/worker ordering, not a real fix, since the same
regex-equivalence argument that classifies its siblings applies to it
unchanged). All 20 named ids are accounted for below.

Invariant-to-test map

Mutant id Rule Kind Resolution
mutmut_8 _returning_was_stripped regex real gap test_returning_was_stripped_matches_lowercase_returning (monkeypatches _SUPPORTS_RETURNING=False, lowercase returning)
mutmut_10 same regex, case-only pattern respell equivalent IGNORECASE folds RETURNING/returning identically
mutmut_38 SERIAL PRIMARY KEY respell equivalent same
mutmut_51 TIMESTAMPTZ respell equivalent same
mutmut_62 DEFAULT NOW() — drops IGNORECASE real gap test_translate_sql_default_now_lowercase_still_translates
mutmut_64 DEFAULT NOW() respell equivalent same
mutmut_78 standalone NOW() respell equivalent same
mutmut_92 UPDATE ... SET alias rule respell equivalent same
mutmut_105 ANY(...) respell equivalent same
mutmut_117 && overlap — drops IGNORECASE real gap test_translate_sql_array_overlap_matches_uppercase_column
mutmut_119 && capture char-class respell equivalent [a-z_] vs [A-Z_] fold identically under IGNORECASE
mutmut_134 xmax drop — drops IGNORECASE real gap test_translate_sql_xmax_drop_matches_uppercase_and_lowercase_as
mutmut_136 xmax drop, ASas respell equivalent same
mutmut_137 xmax drop, xmaxXMAX respell equivalent same
mutmut_149 IS DISTINCT FROM respell equivalent same
mutmut_161 RETURNING strip in _translate_sql — drops IGNORECASE real gap test_translate_sql_strips_lowercase_returning_when_unsupported
mutmut_163 RETURNING strip respell equivalent same
mutmut_175 wiki. flatten, char-class respell equivalent same
mutmut_186 array_length — drops IGNORECASE real gap test_translate_sql_array_length_matches_uppercase
mutmut_188 array_length respell equivalent same

Every "equivalent" classification is backed by a differential harness run
(compile original vs. mutated pattern, run uppercase/lowercase/mixed-case
probes, diff match results — identical in every case) rather than asserted
by inspection alone; the reasoning is also inlined in the new test file's
module docstring.

Completion Ledger

Path introduced Asserting test
_returning_was_stripped matches lowercase returning (IGNORECASE required) test_returning_was_stripped_matches_lowercase_returning
_translate_sql translates lowercase default now() test_translate_sql_default_now_lowercase_still_translates
_translate_sql translates an uppercase &&-overlap column test_translate_sql_array_overlap_matches_uppercase_column
_translate_sql drops XMAX/lowercase-as xmax clause test_translate_sql_xmax_drop_matches_uppercase_and_lowercase_as
_translate_sql strips lowercase returning when unsupported test_translate_sql_strips_lowercase_returning_when_unsupported
_translate_sql translates uppercase ARRAY_LENGTH test_translate_sql_array_length_matches_uppercase
14 equivalent mutants (case-respell under preserved IGNORECASE) documented in the new file's module docstring, empirically verified

Mutation results

  • Runner: mutmut 3.6.0, scoped to mcp_server/infrastructure/sqlite_sql_translate.py
  • Before: 201 mutants, 19 survived (of the issue's 20 named ids; mutmut_136 flip explained above)
  • After: 201 mutants, 187 killed, 14 survived — all 14 documented equivalent, 0 unaccounted-for

Test plan

  • Reproduced the issue's 19/20 survivors on this tree before any fix
  • Added tests_py/infrastructure/test_sqlite_sql_translate_265.py (6 tests, all fail on pre-fix code)
  • Re-ran scoped mutmut: 6 killed, 14 equivalent (verified empirically), 0 unaccounted-for
  • ruff check + ruff format --check clean
  • pyright on the new test file + sqlite_sql_translate.py: 0 errors
  • Full suite: 6609 passed, 117 subtests passed (measured pytest --collect-only -q == 6609, resynced after rebasing onto fix(mutmut): make RepoBadge/Ranking mutation-testable (issue #262, 3rd pass) #283's mutation-testability pass which had itself bumped the count to 6588)
  • Doc-claim/badge counts resynced to the measured absolute: check_doc_claims.py --test-count 6609 and generate_repo_badges.py --check --test-count 6609 both pass
  • All 19 CI jobs green on the exact pushed head (Analyze x3, Build Package, CodeQL, Docker Build x2, Docker Smoke, Fuzz PR batch x2, Lint, Test Python 3.10-3.13, Test SQLite backend, Test Windows, Type Check); mergeable: MERGEABLE, mergeStateStatus: CLEAN against current main

Closes #265

🤖 Generated with Claude Code

https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u

@cdeust
cdeust force-pushed the fix-sqlite-sql-translate-mutants-265 branch from 550b420 to d51c008 Compare July 30, 2026 07:16
cdeust and others added 2 commits July 30, 2026 09:37
…rning

sqlite_compat.py relied on sqlite3's implicit default datetime adapter,
deprecated as of Python 3.12. The one call site binding a raw
`datetime.datetime` parameter is cascade.py::_update_stage_entered
(confirmed the sole such site in this codebase via an instrumented
full-suite run). Registers an explicit `sqlite3.register_adapter`
callback that writes the same "T"-separated ISO-8601 spelling every
other datetime write path in this codebase already uses; old rows
(written in the deprecated adapter's space-separated spelling) keep
reading correctly via `datetime.fromisoformat`, which parses both
spellings identically, so no migration is needed.

Boy-scout: sqlite_compat.py was already 335 lines, over the repo's
300-line cap, before this change touched it. Split the pure SQL-dialect
translation logic (_translate_sql/_returning_was_stripped) into a new
sqlite_sql_translate.py module, bringing sqlite_compat.py to 232 lines.
Also added missing test coverage for _CompatCursor/PsycopgCompatConnection
field wiring surfaced by a scoped mutation run against the touched file
(0 unaccounted survivors; one documented equivalent mutant, see
test_executemany_clears_had_returning_and_reports_real_rowcount).

A pre-existing, unrelated mutation gap in _translate_sql itself (20
survivors, verbatim code predating this change) is filed as #265 per
coding-standards.md §14.3 rather than folded in here.

Closes #260

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
…265)

A scoped mutmut run against `_translate_sql`/`_returning_was_stripped`
surfaced 20 surviving mutants, all the same shape: dropping (or
re-spelling the case of) `flags=re.IGNORECASE` on one of the module's
regex calls. Every existing case-insensitivity fixture happens to
supply an input whose case already matches the pattern's own literal
spelling, so the flag's presence was never observable.

- 6 real gaps closed with new opposite-case tests (mutmut_8, 62, 117,
  134, 161, 186): lowercase `DEFAULT NOW()`, an uppercase `&&`-overlap
  column, `XMAX`/`as` case variants on the xmax-drop rule, a lowercase
  `RETURNING` strip inside `_translate_sql` (distinct from the one
  `_returning_was_stripped` already covered), an uppercase
  `ARRAY_LENGTH`, and lowercase `_returning_was_stripped` input under a
  monkeypatched `_SUPPORTS_RETURNING`.
- 14 documented equivalent mutants (mutmut_10, 38, 51, 64, 78, 92, 105,
  119, 136, 137, 149, 163, 175, 188): the mutation only re-spells the
  pattern's own literal case while `re.IGNORECASE` stays in place, which
  Python's `re` semantics make provably irrelevant to the match —
  confirmed empirically with a differential harness, not asserted by
  inspection.

Re-running the reproduction: 6 killed, 14 equivalent, 0 unaccounted-for
survivors. Suite grows 6560 -> 6591; doc-claim/badge counts resynced to
the measured `pytest --collect-only -q` absolute.

Closes #265

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012Yu6EnWspTfqHoGkExyS6u
@cdeust
cdeust force-pushed the fix-sqlite-sql-translate-mutants-265 branch from d51c008 to 5658933 Compare July 30, 2026 07:44
@cdeust
cdeust merged commit ef42deb into main Jul 30, 2026
19 checks passed
@cdeust
cdeust deleted the fix-sqlite-sql-translate-mutants-265 branch July 30, 2026 08:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sqlite_sql_translate.py: 20 surviving mutants in _translate_sql / _returning_was_stripped

1 participant