"""Migration 035 adds the untracked flag and stays reversible. The downgrade matters more than usual here: dropping the column re-admits every untracked symbol to the live path, and the rows survive either way. A migration that could not roll back cleanly would make the prune change hard to undo on a deploy that went wrong. """ from __future__ import annotations import importlib.util from pathlib import Path import sqlalchemy as sa from alembic.migration import MigrationContext from alembic.operations import Operations def _load_migration_module(): path = ( Path(__file__).resolve().parents[2] / "alembic" / "versions" / "035_ticker_untracked.py" ) spec = importlib.util.spec_from_file_location("migration_035", path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module def _tickers_table(connection: sa.Connection) -> None: connection.execute(sa.text( """ CREATE TABLE tickers ( id INTEGER PRIMARY KEY, symbol VARCHAR(10) NOT NULL, delisted_on DATE ) """ )) connection.execute(sa.text( "INSERT INTO tickers (id, symbol, delisted_on) VALUES (1, 'AAPL', NULL)" )) def _columns(connection: sa.Connection) -> set[str]: return {row[1] for row in connection.execute(sa.text("PRAGMA table_info(tickers)"))} def test_upgrade_adds_a_nullable_untracked_column_and_keeps_existing_rows(): migration = _load_migration_module() engine = sa.create_engine("sqlite://") with engine.begin() as connection: _tickers_table(connection) with Operations.context(MigrationContext.configure(connection)): migration.upgrade() assert "untracked_on" in _columns(connection) # Existing rows default to NULL, i.e. still in the scanner — an upgrade # must not silently drop the whole universe out of the live path. rows = connection.execute( sa.text("SELECT symbol, untracked_on FROM tickers") ).all() assert rows == [("AAPL", None)] def test_downgrade_removes_the_column_without_touching_the_rows(): migration = _load_migration_module() engine = sa.create_engine("sqlite://") with engine.begin() as connection: _tickers_table(connection) with Operations.context(MigrationContext.configure(connection)): migration.upgrade() connection.execute(sa.text( "UPDATE tickers SET untracked_on = '2026-09-07' WHERE symbol = 'AAPL'" )) with Operations.context(MigrationContext.configure(connection)): migration.downgrade() assert "untracked_on" not in _columns(connection) # The ticker itself survives the rollback with its identity intact. assert connection.execute(sa.text("SELECT symbol FROM tickers")).all() == [("AAPL",)]