Warn that the minimum SQLite version will change to 3.40.1 as of 2025.2 (#104298)
Co-authored-by: Robert Resch <robert@resch.dev>pull/122947/head
parent
cddb3bb668
commit
a35fa0e95a
|
@ -16,6 +16,10 @@
|
|||
"backup_failed_out_of_resources": {
|
||||
"title": "Database backup failed due to lack of resources",
|
||||
"description": "The database backup stated at {start_time} failed due to lack of resources. The backup cannot be trusted and must be restarted. This can happen if the database is too large or if the system is under heavy load. Consider upgrading the system hardware or reducing the size of the database by decreasing the number of history days to keep or creating a filter."
|
||||
},
|
||||
"sqlite_too_old": {
|
||||
"title": "Update SQLite to {min_version} or later to continue using the recorder",
|
||||
"description": "Support for version {server_version} of SQLite is ending; the minimum supported version is {min_version}. Please upgrade your database software."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
|
|
@ -96,6 +96,7 @@ MARIADB_WITH_FIXED_IN_QUERIES_108 = _simple_version("10.8.4")
|
|||
MIN_VERSION_MYSQL = _simple_version("8.0.0")
|
||||
MIN_VERSION_PGSQL = _simple_version("12.0")
|
||||
MIN_VERSION_SQLITE = _simple_version("3.31.0")
|
||||
UPCOMING_MIN_VERSION_SQLITE = _simple_version("3.40.1")
|
||||
MIN_VERSION_SQLITE_MODERN_BIND_VARS = _simple_version("3.32.0")
|
||||
|
||||
|
||||
|
@ -356,7 +357,7 @@ def _fail_unsupported_dialect(dialect_name: str) -> NoReturn:
|
|||
raise UnsupportedDialect
|
||||
|
||||
|
||||
def _fail_unsupported_version(
|
||||
def _raise_if_version_unsupported(
|
||||
server_version: str, dialect_name: str, minimum_version: str
|
||||
) -> NoReturn:
|
||||
"""Warn about unsupported database version."""
|
||||
|
@ -373,16 +374,54 @@ def _fail_unsupported_version(
|
|||
raise UnsupportedDialect
|
||||
|
||||
|
||||
@callback
|
||||
def _async_delete_issue_deprecated_version(
|
||||
hass: HomeAssistant, dialect_name: str
|
||||
) -> None:
|
||||
"""Delete the issue about upcoming unsupported database version."""
|
||||
ir.async_delete_issue(hass, DOMAIN, f"{dialect_name}_too_old")
|
||||
|
||||
|
||||
@callback
|
||||
def _async_create_issue_deprecated_version(
|
||||
hass: HomeAssistant,
|
||||
server_version: AwesomeVersion,
|
||||
dialect_name: str,
|
||||
min_version: AwesomeVersion,
|
||||
) -> None:
|
||||
"""Warn about upcoming unsupported database version."""
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"{dialect_name}_too_old",
|
||||
is_fixable=False,
|
||||
severity=ir.IssueSeverity.CRITICAL,
|
||||
translation_key=f"{dialect_name}_too_old",
|
||||
translation_placeholders={
|
||||
"server_version": str(server_version),
|
||||
"min_version": str(min_version),
|
||||
},
|
||||
breaks_in_ha_version="2025.2.0",
|
||||
)
|
||||
|
||||
|
||||
def _extract_version_from_server_response_or_raise(
|
||||
server_response: str,
|
||||
) -> AwesomeVersion:
|
||||
"""Extract version from server response."""
|
||||
return AwesomeVersion(
|
||||
server_response,
|
||||
ensure_strategy=AwesomeVersionStrategy.SIMPLEVER,
|
||||
find_first_match=True,
|
||||
)
|
||||
|
||||
|
||||
def _extract_version_from_server_response(
|
||||
server_response: str,
|
||||
) -> AwesomeVersion | None:
|
||||
"""Attempt to extract version from server response."""
|
||||
try:
|
||||
return AwesomeVersion(
|
||||
server_response,
|
||||
ensure_strategy=AwesomeVersionStrategy.SIMPLEVER,
|
||||
find_first_match=True,
|
||||
)
|
||||
return _extract_version_from_server_response_or_raise(server_response)
|
||||
except AwesomeVersionException:
|
||||
return None
|
||||
|
||||
|
@ -475,13 +514,27 @@ def setup_connection_for_dialect(
|
|||
# as its persistent and isn't free to call every time.
|
||||
result = query_on_connection(dbapi_connection, "SELECT sqlite_version()")
|
||||
version_string = result[0][0]
|
||||
version = _extract_version_from_server_response(version_string)
|
||||
version = _extract_version_from_server_response_or_raise(version_string)
|
||||
|
||||
if not version or version < MIN_VERSION_SQLITE:
|
||||
_fail_unsupported_version(
|
||||
if version < MIN_VERSION_SQLITE:
|
||||
_raise_if_version_unsupported(
|
||||
version or version_string, "SQLite", MIN_VERSION_SQLITE
|
||||
)
|
||||
|
||||
# No elif here since _raise_if_version_unsupported raises
|
||||
if version < UPCOMING_MIN_VERSION_SQLITE:
|
||||
instance.hass.add_job(
|
||||
_async_create_issue_deprecated_version,
|
||||
instance.hass,
|
||||
version or version_string,
|
||||
dialect_name,
|
||||
UPCOMING_MIN_VERSION_SQLITE,
|
||||
)
|
||||
else:
|
||||
instance.hass.add_job(
|
||||
_async_delete_issue_deprecated_version, instance.hass, dialect_name
|
||||
)
|
||||
|
||||
if version and version > MIN_VERSION_SQLITE_MODERN_BIND_VARS:
|
||||
max_bind_vars = SQLITE_MODERN_MAX_BIND_VARS
|
||||
|
||||
|
@ -513,7 +566,7 @@ def setup_connection_for_dialect(
|
|||
|
||||
if is_maria_db:
|
||||
if not version or version < MIN_VERSION_MARIA_DB:
|
||||
_fail_unsupported_version(
|
||||
_raise_if_version_unsupported(
|
||||
version or version_string, "MariaDB", MIN_VERSION_MARIA_DB
|
||||
)
|
||||
if version and (
|
||||
|
@ -529,7 +582,7 @@ def setup_connection_for_dialect(
|
|||
)
|
||||
|
||||
elif not version or version < MIN_VERSION_MYSQL:
|
||||
_fail_unsupported_version(
|
||||
_raise_if_version_unsupported(
|
||||
version or version_string, "MySQL", MIN_VERSION_MYSQL
|
||||
)
|
||||
|
||||
|
@ -551,7 +604,7 @@ def setup_connection_for_dialect(
|
|||
version_string = result[0][0]
|
||||
version = _extract_version_from_server_response(version_string)
|
||||
if not version or version < MIN_VERSION_PGSQL:
|
||||
_fail_unsupported_version(
|
||||
_raise_if_version_unsupported(
|
||||
version or version_string, "PostgreSQL", MIN_VERSION_PGSQL
|
||||
)
|
||||
|
||||
|
|
|
@ -26,6 +26,8 @@ from homeassistant.components.recorder.models import (
|
|||
process_timestamp,
|
||||
)
|
||||
from homeassistant.components.recorder.util import (
|
||||
MIN_VERSION_SQLITE,
|
||||
UPCOMING_MIN_VERSION_SQLITE,
|
||||
end_incomplete_runs,
|
||||
is_second_sunday,
|
||||
resolve_period,
|
||||
|
@ -223,9 +225,9 @@ def test_setup_connection_for_dialect_mysql(mysql_version) -> None:
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"sqlite_version",
|
||||
["3.31.0"],
|
||||
[str(UPCOMING_MIN_VERSION_SQLITE)],
|
||||
)
|
||||
def test_setup_connection_for_dialect_sqlite(sqlite_version) -> None:
|
||||
def test_setup_connection_for_dialect_sqlite(sqlite_version: str) -> None:
|
||||
"""Test setting up the connection for a sqlite dialect."""
|
||||
instance_mock = MagicMock()
|
||||
execute_args = []
|
||||
|
@ -276,10 +278,10 @@ def test_setup_connection_for_dialect_sqlite(sqlite_version) -> None:
|
|||
|
||||
@pytest.mark.parametrize(
|
||||
"sqlite_version",
|
||||
["3.31.0"],
|
||||
[str(UPCOMING_MIN_VERSION_SQLITE)],
|
||||
)
|
||||
def test_setup_connection_for_dialect_sqlite_zero_commit_interval(
|
||||
sqlite_version,
|
||||
sqlite_version: str,
|
||||
) -> None:
|
||||
"""Test setting up the connection for a sqlite dialect with a zero commit interval."""
|
||||
instance_mock = MagicMock(commit_interval=0)
|
||||
|
@ -503,10 +505,6 @@ def test_supported_pgsql(caplog: pytest.LogCaptureFixture, pgsql_version) -> Non
|
|||
"2.0.0",
|
||||
"Version 2.0.0 of SQLite is not supported; minimum supported version is 3.31.0.",
|
||||
),
|
||||
(
|
||||
"dogs",
|
||||
"Version dogs of SQLite is not supported; minimum supported version is 3.31.0.",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_fail_outdated_sqlite(
|
||||
|
@ -725,6 +723,63 @@ async def test_no_issue_for_mariadb_with_MDEV_25020(
|
|||
assert database_engine.optimizer.slow_range_in_select is False
|
||||
|
||||
|
||||
async def test_issue_for_old_sqlite(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
) -> None:
|
||||
"""Test we create and delete an issue for old sqlite versions."""
|
||||
instance_mock = MagicMock()
|
||||
instance_mock.hass = hass
|
||||
execute_args = []
|
||||
close_mock = MagicMock()
|
||||
min_version = str(MIN_VERSION_SQLITE)
|
||||
|
||||
def execute_mock(statement):
|
||||
nonlocal execute_args
|
||||
execute_args.append(statement)
|
||||
|
||||
def fetchall_mock():
|
||||
nonlocal execute_args
|
||||
if execute_args[-1] == "SELECT sqlite_version()":
|
||||
return [[min_version]]
|
||||
return None
|
||||
|
||||
def _make_cursor_mock(*_):
|
||||
return MagicMock(execute=execute_mock, close=close_mock, fetchall=fetchall_mock)
|
||||
|
||||
dbapi_connection = MagicMock(cursor=_make_cursor_mock)
|
||||
|
||||
database_engine = await hass.async_add_executor_job(
|
||||
util.setup_connection_for_dialect,
|
||||
instance_mock,
|
||||
"sqlite",
|
||||
dbapi_connection,
|
||||
True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old")
|
||||
assert issue is not None
|
||||
assert issue.translation_placeholders == {
|
||||
"min_version": str(UPCOMING_MIN_VERSION_SQLITE),
|
||||
"server_version": min_version,
|
||||
}
|
||||
|
||||
min_version = str(UPCOMING_MIN_VERSION_SQLITE)
|
||||
database_engine = await hass.async_add_executor_job(
|
||||
util.setup_connection_for_dialect,
|
||||
instance_mock,
|
||||
"sqlite",
|
||||
dbapi_connection,
|
||||
True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "sqlite_too_old")
|
||||
assert issue is None
|
||||
assert database_engine is not None
|
||||
|
||||
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
async def test_basic_sanity_check(
|
||||
|
|
Loading…
Reference in New Issue