Drop coveragerecords and equivalentscoveragerecords tables (PP-4653) - #3521
dbernstein wants to merge 2 commits into
Conversation
|
Claude finished @dbernstein's task in 2m 44s —— View job Code review
SummaryThis final removal step looks correct. Outside the alembic revisions, nothing in DetailsMinor:
|
|
27bf685 to
ac18fb5
Compare
e5b1dc0 to
c7d813d
Compare
c7d813d to
593e380
Compare
27b613c to
601d261
Compare
## Description Retires the legacy `CoverageProvider` machinery. Overdrive bibliographic coverage now runs as a direct metadata-apply step inside `OverdriveAPI.update_licensepool` instead of routing through `OverdriveBibliographicCoverageProvider.ensure_coverage`. The base `CoverageProvider` classes, the Overdrive/Bibliotheca providers, the coverage-provider runner scripts and their `bin/repair` entry points, and the now-dead readers (`Identifier.missing_coverage_from`, the `Explain` script's coverage block) are deleted. `BibliographicData.apply` no longer writes `CoverageRecord` rows (the `create_coverage_record` parameter is removed; the Celery apply task and the Bibliotheca updater already passed `create_coverage_record=False`). The new `_ensure_bibliographic_coverage` helper wraps `bibliographic.apply(...)` in a `BasePalaceException` guard so bad or incomplete data for a single title is logged and skipped rather than aborting the batch the caller is working through. A failed metadata fetch (Overdrive unreachable, bad credentials) and non-Palace exceptions from `apply` (a `KeyError`, a SQLAlchemy error) are deliberately left to propagate: those affect the whole run and should stop it loudly instead of quietly leaving thousands of pools without Works. The docstring spells that split out. The dead admin `refresh_metadata` endpoint is removed outright, along with the route, its tests, and the two now-unused `METADATA_REFRESH_*` problem details. It was unreachable by any client: `circulation-admin` only renders its "Refresh Metadata" button when the admin OPDS entry carries a `http://librarysimplified.org/terms/rel/refresh` link, and nothing has emitted that rel since the metadata wrangler was removed (#792); `ios-core` never touched the admin API. With no provider wired up, any request that did arrive got an unconditional 400. The `CoverageRecord` model and `coveragerecords` table are intentionally left in place (marked dormant) for one release, per the online-migration / N-1 rule. The stacked follow-up #3521 drops the `coveragerecords` and dormant `equivalentscoveragerecords` tables. > Note: this PR supersedes #3503; it is opened from a branch on the upstream repository and rebased on the latest `main`. ## Motivation and Context JIRA (PP-4468) The `CoverageProvider` batch-processing pattern is being retired in favor of Celery. Investigation showed nothing live reads coverage rows: - The only live caller of `ensure_coverage` (Overdrive's `update_licensepool`) passes `force=True` and ignores the result. - The admin `refresh_metadata` endpoint is wired with no provider and no client can reach it, so it always returned `METADATA_REFRESH_FAILURE`. - `Identifier.missing_coverage_from` / `should_update` / the `Explain` coverage block are only reachable through the already-dead runner scripts. The Celery `bibliographic_apply` task and the Bibliotheca updater already skip coverage-record writes, so Overdrive was the last writer. ## How Has This Been Tested? - `mypy` and the project's `pre-commit` hooks pass on all changed files. - Full-suite collection is clean (6136 tests, no dangling imports), and every `bin/` script's imports were checked to resolve — `bin/` is not imported by CI, which is how the two stale `bin/repair` coverage entry points survived the first pass. - Targeted suites pass under `tox -e py312-docker` (619 tests): Overdrive API including the rewritten `update_licensepool` bibliographic-coverage path and the tests covering the `bibliographic.apply` guard (Palace errors swallowed, non-Palace errors propagating), the admin work-editor and route tests, the Bibliotheca package, data-layer `apply`, admin feeds, scripts, and the affected model tests. ## Checklist - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
e5361af to
9b9dca6
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3521 +/- ##
==========================================
- Coverage 93.74% 93.74% -0.01%
==========================================
Files 510 510
Lines 46417 46390 -27
Branches 6300 6300
==========================================
- Hits 43513 43487 -26
+ Misses 1876 1875 -1
Partials 1028 1028 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
9b9dca6 to
9a206a7
Compare
d67aa37 to
8f46bc2
Compare
9a206a7 to
3c7f990
Compare
d2fa83c to
664289f
Compare
## Description Release-1 half of retiring the `coveragerecords` table, split out of #3521. #3520 retired the CoverageProvider machinery and left the `CoverageRecord` model "dormant" for a release. It wasn't dormant: `Identifier`, `DataSource` and `Collection` still mapped a `coverage_records` relationship, and SQLAlchemy loads a relationship whenever its parent is deleted — to cascade the delete (`mapper.cascade_iterator`) or to null the child's foreign key (`dependency.presort_deletes`). So every `session.delete()` on one of those parents still SELECTed from `coveragerecords`. This PR actually stops using the table: - Removes the `coverage_records` relationships from `Identifier`, `DataSource` and `Collection`, and the `identifier` / `data_source` / `collection` relationships that pointed back at them from `CoverageRecord`. - Reduces `CoverageRecord` to a bare table definition — columns, `__table_args__` and the trailing module-level `Index`. `lookup`, `add_for`, `bulk_add`, `assert_coverage_operation`, `human_readable` and `__repr__` are gone, along with the five `*_OPERATION` constants that only fed them, and `BaseCoverageRecord.not_covered` with the `ALL_STATUSES` / `PREVIOUSLY_ATTEMPTED` / `DEFAULT_COUNT_AS_COVERED` lists that only fed *it*. Nothing called any of them, and they had become a hazard rather than merely dead: with the parent relationships gone, a row written by `add_for`/`bulk_add` could no longer be cleaned up when its parent is deleted, so it would make that delete fail on a foreign key. `status_enum` and the four status constants stay — they are the column type for both dormant models. - Empties `coveragerecords` in a migration. Its foreign keys carry no `ON DELETE` clause, so with the relationships gone SQLAlchemy no longer cleans up children by hand and a surviving row would make deleting its parent fail with a foreign-key violation. The rows are dead data, so `TRUNCATE` is cheaper than teaching a table we drop next release to cascade. It runs under `SET LOCAL lock_timeout = '5s'`: N-1 servers still read the table, so `TRUNCATE`'s `ACCESS EXCLUSIVE` lock can queue behind an in-flight parent delete and block every later reader — failing fast and retrying beats stalling instance startup. - Removes the `CoverageRecord` tests and the `db.coverage_record` fixture, so the next release's backwards-compatibility gate doesn't run them against a schema where the table is gone. The `CoverageRecord` and `EquivalencyCoverageRecord` **models stay** for one more release. A fresh database's schema is built with `create_all` from the models rather than by replaying migrations (`InstanceInitializationScript.initialize_database_schema`), so deleting the classes now would drop the table out from under N-1 servers on new installs. The models and the tables go together in the stacked follow-up, #3521. `equivalentscoveragerecords` needs no change here — it has no ORM relationships pointing at it and its one foreign key already declares `ON DELETE CASCADE`. ## Motivation and Context JIRA (PP-4653) This is the missing middle step in the CoverageProvider retirement. The original plan was #3520 (stop using) → #3521 (drop), but #3520 did not stop the ORM-level reads, so the drop in #3521 would have broken N-1 webservers on any collection or identifier delete during a rolling deploy. Splitting at the relationship rather than at the model is what makes the sequence work: release 1 (this PR) removes the reads but keeps the table in fresh schemas; release 2 (#3521) removes the model and the table together. ## How Has This Been Tested? - `mypy` clean; `pre-commit` clean. - `tests/migration/`, `tests/manager/sqlalchemy/` and `tests/manager/data_layer/` pass under `tox -e py312-docker` (664 tests), including a new migration test covering the truncate and confirming the table itself survives this release. - **Backwards compatibility verified locally**, which is the point of the PR: ``` ./docker/ci/test_backwards_compatibility.sh 7a3274e # the #3520 release → 3346 passed — "The previous release works against the current schema 🎉" ``` For contrast, the same gate run against the old single-PR #3521 failed with **30 tests** erroring on `relation "coveragerecords" does not exist`, 24 of them purely from the relationships being walked on parent deletes. - The stacked #3521 passes the same gate against this branch's content (3391 passed). It needs a rebase onto the current head of this branch before that result applies to the pushed commits. ## Checklist - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Release-2 half of the CoverageProvider retirement. The previous release removed the coverage_records relationships, which is what actually stopped the ORM reading these tables, so N-1 no longer maps either one and both can go. - Remove the CoverageRecord and EquivalencyCoverageRecord models from sqlalchemy/model/coverage.py, leaving the Timestamp model and its separate service_type enum. - Add a migration dropping coveragerecords and equivalentscoveragerecords along with the now-orphaned coverage_status enum; the downgrade recreates both tables with their indexes, constraints and foreign keys. - Adapt the previous release's lock_timeout test. It ran the truncate migration against the schema built by create_all, which no longer has coveragerecords now that the model is gone, so it steps back to that revision first -- the drop's downgrade recreates the table. The test still earns its place: a database older than that revision upgrades through it and this drop in a single transaction, which is exactly the leak the reset guards against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
With the CoverageProvider machinery and the CoverageRecord / EquivalencyCoverageRecord models gone, BaseCoverageRecord had no remaining users except the workcoveragerecords-removal migration (01b1e464a9d1), which imported it only for the coverage_status enum values. Inline those values in that migration so it is self-contained, then drop the mixin. coverage.py now contains only the Timestamp model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3c7f990 to
325e5db
Compare
Description
Follow-up to #3744 ("Stop using the coveragerecords table"), now merged. With no running code mapping or reading the coverage tables, this PR removes the
CoverageRecordand (already-dormant)EquivalencyCoverageRecordmodels and drops thecoveragerecordsandequivalentscoveragerecordstables along with their sharedcoverage_statusenum.It also removes the now-orphaned
BaseCoverageRecordmixin. Its only remaining user was theworkcoveragerecords-removal migration (01b1e464a9d1), which imported it solely for thecoverage_statusenum values; that migration is now self-contained (the enum values are inlined), so the mixin can go.coverage.pyis left containing only theTimestampmodel.Timestampand its separateservice_typeenum are retained.One test from #3744 is adapted here.
test_upgrade_does_not_leak_lock_timeoutran the truncate migration against the schemacreate_allbuilds from the models — which no longer containscoveragerecordsonce this PR deletes the class. It now steps back to that revision first, whose downgrade recreates the table. The test still earns its place after this release: a database older than the truncate revision upgrades through it and this drop in a single transaction, which is exactly the leak thelock_timeoutreset guards against.Motivation and Context
JIRA (PP-4653)
This is the final step of the CoverageProvider retirement, which became a three-release sequence rather than two:
coverage_recordsrelationships, empty the table, drop the model's tests. This is what actually stops the reads.The middle step turned out to be necessary. #3520 left the models in place as "dormant", but the
coverage_recordsrelationships onIdentifier/DataSource/Collectionmeant SQLAlchemy still SELECTed fromcoveragerecordson every parent delete. Running the backwards-compatibility gate against the #3520 release proved it: 30 tests failed withrelation "coveragerecords" does not exist, 24 of them from relationship walks on collection and identifier deletes rather than from any direct use of the model.The split has to fall between the relationship and the model, not around the model, because a fresh database's schema is built with
create_allfrom the models rather than by replaying migrations — so removing the classes any earlier would drop the table out from under N-1 servers on new installs.How Has This Been Tested?
mypyclean;pre-commitclean.upgradedrops both tables and thecoverage_statusenum (leavingtimestamps/service_typeintact);downgraderecreates both tables with identical indexes, constraints, and foreign keys; a secondupgradedrops them again.tests/migration/,tests/manager/sqlalchemy/andtests/manager/data_layer/pass undertox -e py312-docker(666 tests), incl. pytest-alembic single-head / up-down-consistency / model-matches-DDL checks and a drop test. The01b1e464a9d1down-migration still recreatesworkcoveragerecordscorrectly with its inlined enum.lock_timeouttest was re-checked by mutation: with the reset removed from the truncate migration it fails (assert '5s' == '0'), so it still catches the leak rather than passing vacuously.main, which now contains Stop using the coveragerecords table (PP-4653) #3744 and is what N-1 will be:Checklist
🤖 Generated with Claude Code