Guard against db corruption when renaming entities (#112718)

This commit is contained in:
J. Nick Koston 2024-03-08 11:34:07 -10:00 committed by GitHub
parent d868b8d4c5
commit af6f2a516e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 364 additions and 60 deletions

View file

@ -4,7 +4,6 @@ from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable, Iterable, Sequence
import contextlib
import dataclasses
from datetime import datetime, timedelta
from functools import lru_cache, partial
@ -16,7 +15,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast
from sqlalchemy import Select, and_, bindparam, func, lambda_stmt, select, text
from sqlalchemy.engine.row import Row
from sqlalchemy.exc import SQLAlchemyError, StatementError
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm.session import Session
from sqlalchemy.sql.lambdas import StatementLambdaElement
import voluptuous as vol
@ -73,6 +72,7 @@ from .models import (
from .util import (
execute,
execute_stmt_lambda_element,
filter_unique_constraint_integrity_error,
get_instance,
retryable_database_job,
session_scope,
@ -455,7 +455,9 @@ def compile_missing_statistics(instance: Recorder) -> bool:
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
# Find the newest statistics run, if any
if last_run := session.query(func.max(StatisticsRuns.start)).scalar():
@ -487,7 +489,9 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) -
# Return if we already have 5-minute statistics for the requested period
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
modified_statistic_ids = _compile_statistics(
instance, session, start, fire_events
@ -738,7 +742,9 @@ def update_statistics_metadata(
if new_statistic_id is not UNDEFINED and new_statistic_id is not None:
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
statistics_meta_manager.update_statistic_id(
session, DOMAIN, statistic_id, new_statistic_id
@ -2247,54 +2253,6 @@ def async_add_external_statistics(
_async_import_statistics(hass, metadata, statistics)
def _filter_unique_constraint_integrity_error(
instance: Recorder,
) -> Callable[[Exception], bool]:
def _filter_unique_constraint_integrity_error(err: Exception) -> bool:
"""Handle unique constraint integrity errors."""
if not isinstance(err, StatementError):
return False
assert instance.engine is not None
dialect_name = instance.engine.dialect.name
ignore = False
if (
dialect_name == SupportedDialect.SQLITE
and "UNIQUE constraint failed" in str(err)
):
ignore = True
if (
dialect_name == SupportedDialect.POSTGRESQL
and err.orig
and hasattr(err.orig, "pgcode")
and err.orig.pgcode == "23505"
):
ignore = True
if (
dialect_name == SupportedDialect.MYSQL
and err.orig
and hasattr(err.orig, "args")
):
with contextlib.suppress(TypeError):
if err.orig.args[0] == 1062:
ignore = True
if ignore:
_LOGGER.warning(
(
"Blocked attempt to insert duplicated statistic rows, please report"
" at %s"
),
"https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22",
exc_info=err,
)
return ignore
return _filter_unique_constraint_integrity_error
def _import_statistics_with_session(
instance: Recorder,
session: Session,
@ -2398,7 +2356,9 @@ def import_statistics(
with session_scope(
session=instance.get_session(),
exception_filter=_filter_unique_constraint_integrity_error(instance),
exception_filter=filter_unique_constraint_integrity_error(
instance, "statistic"
),
) as session:
return _import_statistics_with_session(
instance, session, metadata, statistics, table