Guard against db corruption when renaming entities (#112718)
This commit is contained in:
parent
d868b8d4c5
commit
af6f2a516e
8 changed files with 364 additions and 60 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue