Skip to content

Commit 93b01a3

Browse files
authored
Fix minimum schema version to run event_id_post_migration (#139014)
* Fix minimum version to run event_id_post_migration The table rebuild to fix the foreign key constraint was added in #120779 but the schema version was not bumped so we need to make sure any database that was created with schema 43 or older still has the migration run as otherwise they will not be able to purge the database with SQLite since each delete in the events table will due a full table scan of the states table to look for a foreign key that is not there fixes #138818 * Apply suggestions from code review * Update homeassistant/components/recorder/migration.py * Update homeassistant/components/recorder/migration.py * Update homeassistant/components/recorder/const.py * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review * Apply suggestions from code review * update tests, add more cover * update tests, add more cover * Update tests/components/recorder/test_migration_run_time_migrations_remember.py
1 parent 98c6a57 commit 93b01a3

File tree

4 files changed

+68
-11
lines changed

4 files changed

+68
-11
lines changed

homeassistant/components/recorder/const.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@
5050
LAST_REPORTED_SCHEMA_VERSION = 43
5151

5252
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
53+
LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION = 43
54+
# https://github.com/home-assistant/core/pull/120779
55+
# fixed the foreign keys in the states table but it did
56+
# not bump the schema version which means only databases
57+
# created with schema 44 and later do not need the rebuild.
5358

5459
INTEGRATION_PLATFORM_COMPILE_STATISTICS = "compile_statistics"
5560
INTEGRATION_PLATFORM_LIST_STATISTIC_IDS = "list_statistic_ids"

homeassistant/components/recorder/migration.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
from .const import (
5353
CONTEXT_ID_AS_BINARY_SCHEMA_VERSION,
5454
EVENT_TYPE_IDS_SCHEMA_VERSION,
55+
LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION,
5556
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION,
5657
STATES_META_SCHEMA_VERSION,
5758
SupportedDialect,
@@ -2490,9 +2491,10 @@ def needs_migrate(self, instance: Recorder, session: Session) -> bool:
24902491
if self.initial_schema_version > self.max_initial_schema_version:
24912492
_LOGGER.debug(
24922493
"Data migration '%s' not needed, database created with version %s "
2493-
"after migrator was added",
2494+
"after migrator was added in version %s",
24942495
self.migration_id,
24952496
self.initial_schema_version,
2497+
self.max_initial_schema_version,
24962498
)
24972499
return False
24982500
if self.start_schema_version < self.required_schema_version:
@@ -2868,7 +2870,14 @@ class EventIDPostMigration(BaseRunTimeMigration):
28682870
"""Migration to remove old event_id index from states."""
28692871

28702872
migration_id = "event_id_post_migration"
2871-
max_initial_schema_version = LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION - 1
2873+
# Note we don't subtract 1 from the max_initial_schema_version
2874+
# in this case because we need to run this migration on databases
2875+
# version >= 43 because the schema was not bumped when the table
2876+
# rebuild was added in
2877+
# https://github.com/home-assistant/core/pull/120779
2878+
# which means its only safe to assume version 44 and later
2879+
# do not need the table rebuild
2880+
max_initial_schema_version = LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION
28722881
task = MigrationTask
28732882
migration_version = 2
28742883

tests/components/recorder/test_migration_from_schema_32.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,7 @@ def _insert_events():
225225
patch.object(recorder, "db_schema", old_db_schema),
226226
patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION),
227227
patch.object(migration.EventsContextIDMigration, "migrate_data"),
228+
patch.object(migration.EventIDPostMigration, "migrate_data"),
228229
patch(CREATE_ENGINE_TARGET, new=_create_engine_test),
229230
):
230231
async with (
@@ -282,6 +283,7 @@ def _fetch_migrated_events():
282283
patch(
283284
"sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create
284285
) as wrapped_idx_create,
286+
patch.object(migration.EventIDPostMigration, "migrate_data"),
285287
):
286288
async with async_test_recorder(
287289
hass, wait_recorder=False, wait_recorder_setup=False
@@ -588,6 +590,7 @@ def _insert_states():
588590
patch.object(recorder, "db_schema", old_db_schema),
589591
patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION),
590592
patch.object(migration.StatesContextIDMigration, "migrate_data"),
593+
patch.object(migration.EventIDPostMigration, "migrate_data"),
591594
patch(CREATE_ENGINE_TARGET, new=_create_engine_test),
592595
):
593596
async with (
@@ -640,6 +643,7 @@ def _fetch_migrated_states():
640643
patch(
641644
"sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create
642645
) as wrapped_idx_create,
646+
patch.object(migration.EventIDPostMigration, "migrate_data"),
643647
):
644648
async with async_test_recorder(
645649
hass, wait_recorder=False, wait_recorder_setup=False
@@ -1127,6 +1131,7 @@ def _insert_events():
11271131
patch.object(migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION),
11281132
patch.object(migration.EntityIDMigration, "migrate_data"),
11291133
patch.object(migration.EntityIDPostMigration, "migrate_data"),
1134+
patch.object(migration.EventIDPostMigration, "migrate_data"),
11301135
patch(CREATE_ENGINE_TARGET, new=_create_engine_test),
11311136
):
11321137
async with (
@@ -1158,9 +1163,12 @@ def _fetch_migrated_states():
11581163
return {state.state: state.entity_id for state in states}
11591164

11601165
# Run again with new schema, let migration run
1161-
with patch(
1162-
"sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create
1163-
) as wrapped_idx_create:
1166+
with (
1167+
patch(
1168+
"sqlalchemy.schema.Index.create", autospec=True, wraps=Index.create
1169+
) as wrapped_idx_create,
1170+
patch.object(migration.EventIDPostMigration, "migrate_data"),
1171+
):
11641172
async with (
11651173
async_test_home_assistant() as hass,
11661174
async_test_recorder(hass) as instance,
@@ -1169,7 +1177,6 @@ def _fetch_migrated_states():
11691177

11701178
await hass.async_block_till_done()
11711179
await async_wait_recording_done(hass)
1172-
await async_wait_recording_done(hass)
11731180

11741181
states_by_state = await instance.async_add_executor_job(
11751182
_fetch_migrated_states

tests/components/recorder/test_migration_run_time_migrations_remember.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ def _create_engine_test(*args, **kwargs):
115115
"event_context_id_as_binary": (0, 1),
116116
"event_type_id_migration": (2, 1),
117117
"entity_id_migration": (2, 1),
118-
"event_id_post_migration": (0, 0),
118+
"event_id_post_migration": (1, 1),
119119
"entity_id_post_migration": (0, 1),
120120
},
121121
[
@@ -131,7 +131,7 @@ def _create_engine_test(*args, **kwargs):
131131
"event_context_id_as_binary": (0, 0),
132132
"event_type_id_migration": (2, 1),
133133
"entity_id_migration": (2, 1),
134-
"event_id_post_migration": (0, 0),
134+
"event_id_post_migration": (1, 1),
135135
"entity_id_post_migration": (0, 1),
136136
},
137137
["ix_states_entity_id_last_updated_ts"],
@@ -143,13 +143,43 @@ def _create_engine_test(*args, **kwargs):
143143
"event_context_id_as_binary": (0, 0),
144144
"event_type_id_migration": (0, 0),
145145
"entity_id_migration": (2, 1),
146-
"event_id_post_migration": (0, 0),
146+
"event_id_post_migration": (1, 1),
147147
"entity_id_post_migration": (0, 1),
148148
},
149149
["ix_states_entity_id_last_updated_ts"],
150150
),
151151
(
152152
38,
153+
{
154+
"state_context_id_as_binary": (0, 0),
155+
"event_context_id_as_binary": (0, 0),
156+
"event_type_id_migration": (0, 0),
157+
"entity_id_migration": (0, 0),
158+
"event_id_post_migration": (1, 1),
159+
"entity_id_post_migration": (0, 0),
160+
},
161+
[],
162+
),
163+
(
164+
43,
165+
{
166+
"state_context_id_as_binary": (0, 0),
167+
"event_context_id_as_binary": (0, 0),
168+
"event_type_id_migration": (0, 0),
169+
"entity_id_migration": (0, 0),
170+
# Schema was not bumped when the SQLite
171+
# table rebuild was implemented so we need
172+
# run event_id_post_migration up until
173+
# schema 44 since its the first one we can
174+
# be sure has the foreign key constraint was removed
175+
# via https://github.com/home-assistant/core/pull/120779
176+
"event_id_post_migration": (1, 1),
177+
"entity_id_post_migration": (0, 0),
178+
},
179+
[],
180+
),
181+
(
182+
44,
153183
{
154184
"state_context_id_as_binary": (0, 0),
155185
"event_context_id_as_binary": (0, 0),
@@ -266,8 +296,14 @@ def patch_migrate(
266296
# the expected number of times.
267297
for migrator, mock in migrator_mocks.items():
268298
needs_migrate_calls, migrate_data_calls = expected_migrator_calls[migrator]
269-
assert len(mock["needs_migrate"].mock_calls) == needs_migrate_calls
270-
assert len(mock["migrate_data"].mock_calls) == migrate_data_calls
299+
assert len(mock["needs_migrate"].mock_calls) == needs_migrate_calls, (
300+
f"Expected {migrator} needs_migrate to be called {needs_migrate_calls} times,"
301+
f" got {len(mock['needs_migrate'].mock_calls)}"
302+
)
303+
assert len(mock["migrate_data"].mock_calls) == migrate_data_calls, (
304+
f"Expected {migrator} migrate_data to be called {migrate_data_calls} times, "
305+
f"got {len(mock['migrate_data'].mock_calls)}"
306+
)
271307

272308

273309
@pytest.mark.parametrize("enable_migrate_state_context_ids", [True])

0 commit comments

Comments
 (0)