diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 5d650ec83e2..e558d19b530 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -10,6 +10,7 @@ from homeassistant.helpers.json import JSONEncoder DATA_INSTANCE = "recorder_instance" SQLITE_URL_PREFIX = "sqlite://" +MYSQLDB_URL_PREFIX = "mysql://" DOMAIN = "recorder" CONF_DB_INTEGRITY_CHECK = "db_integrity_check" diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index a008ae6767b..ba0e5ba17d1 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -41,6 +41,7 @@ from .const import ( DB_WORKER_PREFIX, KEEPALIVE_TIME, MAX_QUEUE_BACKLOG, + MYSQLDB_URL_PREFIX, SQLITE_URL_PREFIX, SupportedDialect, ) @@ -77,6 +78,7 @@ from .tasks import ( WaitTask, ) from .util import ( + build_mysqldb_conv, dburl_to_path, end_incomplete_runs, is_second_sunday, @@ -1014,6 +1016,14 @@ class Recorder(threading.Thread): kwargs["pool_reset_on_return"] = None elif self.db_url.startswith(SQLITE_URL_PREFIX): kwargs["poolclass"] = RecorderPool + elif self.db_url.startswith(MYSQLDB_URL_PREFIX): + # If they have configured MySQLDB but don't have + # the MySQLDB module installed this will throw + # an ImportError which we suppress here since + # sqlalchemy will give them a better error when + # it tried to import it below. + with contextlib.suppress(ImportError): + kwargs["connect_args"] = {"conv": build_mysqldb_conv()} else: kwargs["echo"] = False diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index 9f6bef86a29..f48a6126ea9 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -15,6 +15,7 @@ from awesomeversion import ( AwesomeVersionException, AwesomeVersionStrategy, ) +import ciso8601 from sqlalchemy import text from sqlalchemy.engine.cursor import CursorFetchStrategy from sqlalchemy.exc import OperationalError, SQLAlchemyError @@ -331,6 +332,30 @@ def _extract_version_from_server_response( return None +def _datetime_or_none(value: str) -> datetime | None: + """Fast version of mysqldb DateTime_or_None. + + https://github.com/PyMySQL/mysqlclient/blob/v2.1.0/MySQLdb/times.py#L66 + """ + try: + return ciso8601.parse_datetime(value) + except ValueError: + return None + + +def build_mysqldb_conv() -> dict: + """Build a MySQLDB conv dict that uses cisco8601 to parse datetimes.""" + # Late imports since we only call this if they are using mysqldb + from MySQLdb.constants import ( # pylint: disable=import-outside-toplevel,import-error + FIELD_TYPE, + ) + from MySQLdb.converters import ( # pylint: disable=import-outside-toplevel,import-error + conversions, + ) + + return {**conversions, FIELD_TYPE.DATETIME: _datetime_or_none} + + def setup_connection_for_dialect( instance: Recorder, dialect_name: str, diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 6b1093ee038..237030c1186 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta import os import sqlite3 -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, Mock, patch import pytest from sqlalchemy import text @@ -641,3 +641,20 @@ def test_is_second_sunday(): assert is_second_sunday(datetime(2022, 5, 8, 0, 0, 0, tzinfo=dt_util.UTC)) is True assert is_second_sunday(datetime(2022, 1, 10, 0, 0, 0, tzinfo=dt_util.UTC)) is False + + +def test_build_mysqldb_conv(): + """Test building the MySQLdb connect conv param.""" + mock_converters = Mock(conversions={"original": "preserved"}) + mock_constants = Mock(FIELD_TYPE=Mock(DATETIME="DATETIME")) + with patch.dict( + "sys.modules", + **{"MySQLdb.constants": mock_constants, "MySQLdb.converters": mock_converters}, + ): + conv = util.build_mysqldb_conv() + + assert conv["original"] == "preserved" + assert conv["DATETIME"]("INVALID") is None + assert conv["DATETIME"]("2022-05-13T22:33:12.741") == datetime( + 2022, 5, 13, 22, 33, 12, 741000, tzinfo=None + )