Skip to content

Commit 0cf0f10

Browse files
Correct migration to recorder schema 51 (home-assistant#156267)
Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 8429f15 commit 0cf0f10

File tree

6 files changed

+1489
-7
lines changed

6 files changed

+1489
-7
lines changed

homeassistant/components/recorder/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
EVENT_TYPE_IDS_SCHEMA_VERSION = 37
5555
STATES_META_SCHEMA_VERSION = 38
5656
CIRCULAR_MEAN_SCHEMA_VERSION = 49
57-
UNIT_CLASS_SCHEMA_VERSION = 51
57+
UNIT_CLASS_SCHEMA_VERSION = 52
5858

5959
LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28
6060
LEGACY_STATES_EVENT_FOREIGN_KEYS_FIXED_SCHEMA_VERSION = 43

homeassistant/components/recorder/db_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class LegacyBase(DeclarativeBase):
7171
"""Base class for tables, used for schema migration."""
7272

7373

74-
SCHEMA_VERSION = 51
74+
SCHEMA_VERSION = 52
7575

7676
_LOGGER = logging.getLogger(__name__)
7777

homeassistant/components/recorder/migration.py

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,15 @@
1313
from uuid import UUID
1414

1515
import sqlalchemy
16-
from sqlalchemy import ForeignKeyConstraint, MetaData, Table, func, text, update
16+
from sqlalchemy import (
17+
ForeignKeyConstraint,
18+
MetaData,
19+
Table,
20+
cast as cast_,
21+
func,
22+
text,
23+
update,
24+
)
1725
from sqlalchemy.engine import CursorResult, Engine
1826
from sqlalchemy.exc import (
1927
DatabaseError,
@@ -26,8 +34,9 @@
2634
from sqlalchemy.orm import DeclarativeBase
2735
from sqlalchemy.orm.session import Session
2836
from sqlalchemy.schema import AddConstraint, CreateTable, DropConstraint
29-
from sqlalchemy.sql.expression import true
37+
from sqlalchemy.sql.expression import and_, true
3038
from sqlalchemy.sql.lambdas import StatementLambdaElement
39+
from sqlalchemy.types import BINARY
3140

3241
from homeassistant.core import HomeAssistant
3342
from homeassistant.util.enum import try_parse_enum
@@ -2044,14 +2053,74 @@ def _apply_update(self) -> None:
20442053
class _SchemaVersion51Migrator(_SchemaVersionMigrator, target_version=51):
20452054
def _apply_update(self) -> None:
20462055
"""Version specific update method."""
2047-
# Add unit class column to StatisticsMeta
2056+
# Replaced with version 52 which corrects issues with MySQL string comparisons.
2057+
2058+
2059+
class _SchemaVersion52Migrator(_SchemaVersionMigrator, target_version=52):
2060+
def _apply_update(self) -> None:
2061+
"""Version specific update method."""
2062+
if self.engine.dialect.name == SupportedDialect.MYSQL:
2063+
self._apply_update_mysql()
2064+
else:
2065+
self._apply_update_postgresql_sqlite()
2066+
2067+
def _apply_update_mysql(self) -> None:
2068+
"""Version specific update method for mysql."""
2069+
_add_columns(self.session_maker, "statistics_meta", ["unit_class VARCHAR(255)"])
2070+
with session_scope(session=self.session_maker()) as session:
2071+
connection = session.connection()
2072+
for conv in _PRIMARY_UNIT_CONVERTERS:
2073+
case_sensitive_units = {
2074+
u.encode("utf-8") if u else u for u in conv.VALID_UNITS
2075+
}
2076+
# Reset unit_class to None for entries that do not match
2077+
# the valid units (case sensitive) but matched before due to
2078+
# case insensitive comparisons.
2079+
connection.execute(
2080+
update(StatisticsMeta)
2081+
.where(
2082+
and_(
2083+
StatisticsMeta.unit_of_measurement.in_(conv.VALID_UNITS),
2084+
cast_(StatisticsMeta.unit_of_measurement, BINARY).not_in(
2085+
case_sensitive_units
2086+
),
2087+
)
2088+
)
2089+
.values(unit_class=None)
2090+
)
2091+
# Do an explicitly case sensitive match (actually binary) to set the
2092+
# correct unit_class. This is needed because we use the case sensitive
2093+
# utf8mb4_unicode_ci collation.
2094+
connection.execute(
2095+
update(StatisticsMeta)
2096+
.where(
2097+
and_(
2098+
cast_(StatisticsMeta.unit_of_measurement, BINARY).in_(
2099+
case_sensitive_units
2100+
),
2101+
StatisticsMeta.unit_class.is_(None),
2102+
)
2103+
)
2104+
.values(unit_class=conv.UNIT_CLASS)
2105+
)
2106+
2107+
def _apply_update_postgresql_sqlite(self) -> None:
2108+
"""Version specific update method for postgresql and sqlite."""
20482109
_add_columns(self.session_maker, "statistics_meta", ["unit_class VARCHAR(255)"])
20492110
with session_scope(session=self.session_maker()) as session:
20502111
connection = session.connection()
20512112
for conv in _PRIMARY_UNIT_CONVERTERS:
2113+
# Set the correct unit_class. Unlike MySQL, Postgres and SQLite
2114+
# have case sensitive string comparisons by default, so we
2115+
# can directly match on the valid units.
20522116
connection.execute(
20532117
update(StatisticsMeta)
2054-
.where(StatisticsMeta.unit_of_measurement.in_(conv.VALID_UNITS))
2118+
.where(
2119+
and_(
2120+
StatisticsMeta.unit_of_measurement.in_(conv.VALID_UNITS),
2121+
StatisticsMeta.unit_class.is_(None),
2122+
)
2123+
)
20552124
.values(unit_class=conv.UNIT_CLASS)
20562125
)
20572126

0 commit comments

Comments
 (0)