@@ -1929,3 +1929,110 @@ def test_migrate_db_skips_backfills_when_flag_set(self) -> None:
19291929 _count_annotations (session_factory = session_factory , key = pipeline_name_key )
19301930 == 0
19311931 )
1932+
1933+
1934+ # ---------------------------------------------------------------------------
1935+ # secret_value column migration
1936+ # ---------------------------------------------------------------------------
1937+
1938+
1939+ def test_migrate_secret_value_column_idempotent (
1940+ caplog : pytest .LogCaptureFixture ,
1941+ ) -> None :
1942+ """First call ALTERs VARCHAR(255) to TEXT; second call skips (already TEXT)."""
1943+ db_engine = database_ops .create_db_engine (database_uri = "sqlite://" )
1944+ bts ._TableBase .metadata .create_all (db_engine )
1945+
1946+ varchar_255 = sqlalchemy .types .String (length = 255 )
1947+ text_type = sqlalchemy .types .Text ()
1948+
1949+ call_count = 0
1950+
1951+ def _fake_get_columns (
1952+ * ,
1953+ table_name : str ,
1954+ ) -> list [dict [str , Any ]]:
1955+ nonlocal call_count
1956+ call_count += 1
1957+ fake_type = varchar_255 if call_count == 1 else text_type
1958+ return [{"name" : "secret_value" , "type" : fake_type }]
1959+
1960+ mock_conn = mock .MagicMock ()
1961+ mock_connect = mock .MagicMock ()
1962+ mock_connect .__enter__ = mock .MagicMock (return_value = mock_conn )
1963+ mock_connect .__exit__ = mock .MagicMock (return_value = False )
1964+
1965+ real_dialect = db_engine .dialect
1966+ fake_dialect = mock .MagicMock (wraps = real_dialect )
1967+ fake_dialect .name = "mysql"
1968+ fake_dialect .type_compiler_instance = real_dialect .type_compiler_instance
1969+
1970+ with (
1971+ mock .patch .object (db_engine , "dialect" , fake_dialect ),
1972+ mock .patch ("sqlalchemy.inspect" ) as mock_inspect ,
1973+ mock .patch .object (db_engine , "connect" , return_value = mock_connect ),
1974+ caplog .at_level (logging .INFO ),
1975+ ):
1976+ mock_inspect .return_value .get_columns .side_effect = (
1977+ lambda table_name : _fake_get_columns (table_name = table_name )
1978+ )
1979+
1980+ database_migrations .migrate_secret_value_column (db_engine = db_engine )
1981+ database_migrations .migrate_secret_value_column (db_engine = db_engine )
1982+
1983+ msgs = caplog .messages
1984+ assert len (msgs ) == 4
1985+ assert (
1986+ "current_type=VARCHAR(255), current_length=255, target_type=TEXT, dialect=mysql"
1987+ in msgs [0 ]
1988+ )
1989+ assert (
1990+ "executing SQL: ALTER TABLE secret MODIFY COLUMN secret_value TEXT" in msgs [1 ]
1991+ )
1992+ assert "complete" in msgs [2 ]
1993+ assert "skipped (already TEXT)" in msgs [3 ]
1994+ mock_conn .execute .assert_called_once ()
1995+ mock_conn .commit .assert_called_once ()
1996+
1997+
1998+ def test_migrate_secret_value_column_mysql_alter_sql (
1999+ caplog : pytest .LogCaptureFixture ,
2000+ ) -> None :
2001+ """Mock MySQL dialect and verify the exact ALTER SQL generated."""
2002+ db_engine = database_ops .create_db_engine (database_uri = "sqlite://" )
2003+ bts ._TableBase .metadata .create_all (db_engine )
2004+
2005+ real_dialect = db_engine .dialect
2006+
2007+ def _fake_get_columns (
2008+ * ,
2009+ table_name : str ,
2010+ ) -> list [dict [str , Any ]]:
2011+ return [{"name" : "secret_value" , "type" : sqlalchemy .types .String (length = 255 )}]
2012+
2013+ mock_conn = mock .MagicMock ()
2014+ mock_connect = mock .MagicMock ()
2015+ mock_connect .__enter__ = mock .MagicMock (return_value = mock_conn )
2016+ mock_connect .__exit__ = mock .MagicMock (return_value = False )
2017+
2018+ fake_dialect = mock .MagicMock (wraps = real_dialect )
2019+ fake_dialect .name = "mysql"
2020+ fake_dialect .type_compiler_instance = real_dialect .type_compiler_instance
2021+
2022+ with (
2023+ mock .patch .object (db_engine , "dialect" , fake_dialect ),
2024+ mock .patch ("sqlalchemy.inspect" ) as mock_inspect ,
2025+ mock .patch .object (db_engine , "connect" , return_value = mock_connect ),
2026+ caplog .at_level (logging .INFO ),
2027+ ):
2028+ mock_inspect .return_value .get_columns .side_effect = (
2029+ lambda table_name : _fake_get_columns (table_name = table_name )
2030+ )
2031+
2032+ database_migrations .migrate_secret_value_column (db_engine = db_engine )
2033+
2034+ executed_sql = mock_conn .execute .call_args [0 ][0 ].text
2035+ assert executed_sql == "ALTER TABLE secret MODIFY COLUMN secret_value TEXT"
2036+ assert any ("current_length=255" in msg for msg in caplog .messages )
2037+ assert any ("dialect=mysql" in msg for msg in caplog .messages )
2038+ mock_conn .commit .assert_called_once ()
0 commit comments