"""Disable sources on retired platforms, so the scheduler stops selecting them. Milestone #406, phase 1 (switch pixiv off). Rule #171 records the scope decision. ## Why this is a migration and not a button The live instance had one pixiv source still ENABLED when pixiv was retired (read 2026-09-13, step 1) even though the operator believed it gone. Unregistering a platform removes it from code; it does not touch the `source` rows that name it. Left enabled, that row keeps being picked by the scheduler every interval, and `download_backends` now refuses it with `unsupported_url` — forever, as a climbing failure count on a source the operator has already given up. A migration reaches the live instance on deploy without depending on anyone finding the row and clicking it. The `run_download` guard is what makes a stale enabled row SAFE; this is what makes it QUIET. ## Deliberately NOT done here - **No rows are deleted.** Deleting a source sets its posts' `source_id` to NULL (FK `ON DELETE SET NULL`), and `uq_post_artist_external_id_null_source` can reject that if a source-less copy of one of those posts already exists. That needs checking against real data first, which is phase 2's job (step 6). A disable cannot collide with anything. - **No posts or images are touched.** The art stays. - **deviantart is included** because #3069 retired it and nothing disabled its rows either. The read found none, so for it this is a no-op — written anyway, so the statement names every retired platform rather than just the latest one. ## Hardcoded platform names A migration is a record of one event, frozen in time, so it names the platforms it acted on rather than importing today's registry — the registry will keep changing and this revision must not. Revision ID: 0097 Revises: 0096 Create Date: 2026-09-13 """ from typing import Sequence, Union from alembic import op revision: str = "0097" down_revision: Union[str, None] = "0096" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # Clears the failure state the same way `SourceService.update` does when a # source is disabled through the app (issue #1285), so a retired source # does not keep showing as failing after it stops being polled. A disable # done here and one done by clicking must leave identical rows. op.execute( "UPDATE source SET enabled = false, last_error = NULL, " "error_type = NULL, consecutive_failures = 0 " "WHERE enabled AND platform IN ('pixiv', 'deviantart')" ) def downgrade() -> None: # Irreversible by design: which of these rows were enabled before is not # recorded, and re-enabling every retired-platform source would resume # polling services the product no longer supports. Rule #22 owes no # migration story back to a dropped platform. pass