Use positional row access in recorder history queries (#175532)

This commit is contained in:
Franck Nijhof
2026-08-30 20:37:15 +02:00
committed by GitHub
parent d3fc4e6552
commit a82743cf65
3 changed files with 125 additions and 99 deletions
@@ -83,6 +83,28 @@ def _stmt_and_join_attributes(
return _select
def _row_field_indices(
no_attributes: bool,
include_last_changed: bool,
include_last_reported: bool,
) -> tuple[int | None, int | None, int | None]:
"""Return the (last_changed_ts, last_reported_ts, attributes) row indices.
Must match the column order selected by _stmt_and_join_attributes.
"""
next_idx = len(_FIELD_MAP)
last_changed_ts_idx = last_reported_ts_idx = attributes_idx = None
if include_last_changed:
last_changed_ts_idx = next_idx
next_idx += 1
if include_last_reported:
last_reported_ts_idx = next_idx
next_idx += 1
if not no_attributes:
attributes_idx = next_idx
return last_changed_ts_idx, last_reported_ts_idx, attributes_idx
def _stmt_and_join_attributes_for_start_state(
no_attributes: bool,
include_last_changed: bool,
@@ -316,6 +338,9 @@ def get_significant_states_with_session(
minimal_response,
compressed_state_format,
no_attributes=no_attributes,
field_indices=_row_field_indices(
no_attributes, not significant_changes_only, False
),
)
@@ -513,6 +538,7 @@ def state_changes_during_period(
entity_id_to_metadata_id,
descending=descending,
no_attributes=no_attributes,
field_indices=_row_field_indices(no_attributes, False, True),
),
)
@@ -607,6 +633,7 @@ def get_last_state_changes(
entity_ids,
entity_id_to_metadata_id,
no_attributes=False,
field_indices=_row_field_indices(False, False, number_of_states > 1),
),
)
@@ -796,6 +823,8 @@ def _sorted_states_to_dict(
compressed_state_format: bool = False,
descending: bool = False,
no_attributes: bool = False,
*,
field_indices: tuple[int | None, int | None, int | None],
) -> dict[str, list[State | dict[str, Any]]]:
"""Convert SQL results into JSON friendly data structure.
@@ -809,8 +838,19 @@ def _sorted_states_to_dict(
axis correctly.
"""
field_map = _FIELD_MAP
last_changed_ts_idx, last_reported_ts_idx, attributes_idx = field_indices
state_class: Callable[
[Row, dict[str, dict[str, Any]], float | None, str, str, float | None, bool],
[
dict[str, dict[str, Any]],
float | None,
str,
str,
float | None,
Any,
float | None,
float | None,
bool,
],
State | dict[str, Any],
]
if compressed_state_format:
@@ -856,12 +896,18 @@ def _sorted_states_to_dict(
ent_results.extend(
[
state_class(
db_state,
attr_cache,
start_time_ts,
entity_id,
db_state[state_idx],
db_state[last_updated_ts_idx],
None if attributes_idx is None else db_state[attributes_idx],
None
if last_changed_ts_idx is None
else db_state[last_changed_ts_idx],
None
if last_reported_ts_idx is None
else db_state[last_reported_ts_idx],
False,
)
for db_state in group
@@ -880,12 +926,18 @@ def _sorted_states_to_dict(
prev_state = first_state[state_idx]
ent_results.append(
state_class(
first_state,
attr_cache,
start_time_ts,
entity_id,
prev_state,
first_state[last_updated_ts_idx],
None if attributes_idx is None else first_state[attributes_idx],
None
if last_changed_ts_idx is None
else first_state[last_changed_ts_idx],
None
if last_reported_ts_idx is None
else first_state[last_reported_ts_idx],
no_attributes,
)
)
@@ -5,7 +5,6 @@ import logging
from typing import TYPE_CHECKING, Any, override
from propcache.api import cached_property
from sqlalchemy.engine.row import Row
from homeassistant.const import (
COMPRESSED_STATE_ATTRIBUTES,
@@ -39,20 +38,24 @@ class LazyState(State):
def __init__( # pylint: disable=super-init-not-called
self,
row: Row,
attr_cache: dict[str, dict[str, Any]],
start_time_ts: float | None,
entity_id: str,
state: str,
last_updated_ts: float | None,
no_attributes: bool,
attributes_source: Any = None,
last_changed_ts: float | None = None,
last_reported_ts: float | None = None,
no_attributes: bool = False,
) -> None:
"""Init the lazy state."""
self._row = row
self._attributes_source = attributes_source
self.entity_id = entity_id
self.state = state or ""
self._attributes: dict[str, Any] | None = None
self._last_updated_ts: float | None = last_updated_ts or start_time_ts
self._last_changed_ts = last_changed_ts
self._last_reported_ts = last_reported_ts
self.attr_cache = attr_cache
self.context = EMPTY_CONTEXT
@@ -60,14 +63,7 @@ class LazyState(State):
@override
def attributes(self) -> dict[str, Any]: # type: ignore[override]
"""State attributes."""
return decode_attributes_from_source(
getattr(self._row, "attributes", None), self.attr_cache
)
@cached_property
def _last_changed_ts(self) -> float | None:
"""Last changed timestamp."""
return getattr(self._row, "last_changed_ts", None)
return decode_attributes_from_source(self._attributes_source, self.attr_cache)
@cached_property
@override
@@ -77,11 +73,6 @@ class LazyState(State):
self._last_changed_ts or self._last_updated_ts # type: ignore[arg-type]
)
@cached_property
def _last_reported_ts(self) -> float | None:
"""Last reported timestamp."""
return getattr(self._row, "last_reported_ts", None)
@cached_property
@override
def last_reported(self) -> datetime: # type: ignore[override]
@@ -147,26 +138,24 @@ class LazyState(State):
def row_to_compressed_state(
row: Row,
attr_cache: dict[str, dict[str, Any]],
start_time_ts: float | None,
entity_id: str,
state: str,
last_updated_ts: float | None,
no_attributes: bool,
attributes_source: Any = None,
last_changed_ts: float | None = None,
last_reported_ts: float | None = None,
no_attributes: bool = False,
) -> dict[str, Any]:
"""Convert a database row to a compressed state schema 41 and later."""
comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state}
if not no_attributes:
comp_state[COMPRESSED_STATE_ATTRIBUTES] = decode_attributes_from_source(
getattr(row, "attributes", None), attr_cache
attributes_source, attr_cache
)
row_last_updated_ts: float = last_updated_ts or start_time_ts # type: ignore[assignment]
comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts
if (
(row_last_changed_ts := getattr(row, "last_changed_ts", None))
and row_last_changed_ts
and row_last_updated_ts != row_last_changed_ts
):
comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_last_changed_ts
if last_changed_ts and row_last_updated_ts != last_changed_ts:
comp_state[COMPRESSED_STATE_LAST_CHANGED] = last_changed_ts
return comp_state
+55 -70
View File
@@ -1,7 +1,6 @@
"""The tests for the Recorder component."""
from datetime import datetime, timedelta
from unittest.mock import PropertyMock
import pytest
@@ -301,42 +300,31 @@ async def test_lazy_state_handles_include_json(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that the LazyState class handles invalid json."""
row = PropertyMock(
entity_id="sensor.invalid",
shared_attrs="{INVALID_JSON}",
)
assert LazyState(row, {}, None, row.entity_id, "", 1, False).attributes == {}
lstate = LazyState({}, None, "sensor.invalid", "", 1, "{INVALID_JSON}")
assert lstate.attributes == {}
assert "Error converting row to state attributes" in caplog.text
async def test_lazy_state_can_decode_attributes(
caplog: pytest.LogCaptureFixture,
) -> None:
async def test_lazy_state_can_decode_attributes() -> None:
"""Test that the LazyState prefers can decode attributes."""
row = PropertyMock(
entity_id="sensor.invalid",
attributes='{"shared":true}',
)
assert LazyState(row, {}, None, row.entity_id, "", 1, False).attributes == {
"shared": True
}
lstate = LazyState({}, None, "sensor.invalid", "", 1, '{"shared":true}')
assert lstate.attributes == {"shared": True}
async def test_lazy_state_handles_different_last_updated_and_last_changed(
caplog: pytest.LogCaptureFixture,
) -> None:
async def test_lazy_state_handles_different_last_updated_and_last_changed() -> None:
"""Test that the LazyState handles different last_updated and last_changed."""
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
row = PropertyMock(
entity_id="sensor.valid",
state="off",
attributes='{"shared":true}',
last_updated_ts=now.timestamp(),
last_reported_ts=now.timestamp(),
last_changed_ts=(now - timedelta(seconds=60)).timestamp(),
)
last_updated_ts = now.timestamp()
last_changed_ts = (now - timedelta(seconds=60)).timestamp()
lstate = LazyState(
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
{},
None,
"sensor.valid",
"off",
last_updated_ts,
'{"shared":true}',
last_changed_ts,
last_updated_ts,
)
assert lstate.as_dict() == {
"attributes": {"shared": True},
@@ -345,9 +333,9 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(
"last_updated": "2021-06-12T03:04:01.000323+00:00",
"state": "off",
}
assert lstate.last_updated.timestamp() == row.last_updated_ts
assert lstate.last_changed.timestamp() == row.last_changed_ts
assert lstate.last_reported.timestamp() == row.last_updated_ts
assert lstate.last_updated.timestamp() == last_updated_ts
assert lstate.last_changed.timestamp() == last_changed_ts
assert lstate.last_reported.timestamp() == last_updated_ts
assert lstate.as_dict() == {
"attributes": {"shared": True},
"entity_id": "sensor.valid",
@@ -355,26 +343,24 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(
"last_updated": "2021-06-12T03:04:01.000323+00:00",
"state": "off",
}
assert lstate.last_changed_timestamp == row.last_changed_ts
assert lstate.last_updated_timestamp == row.last_updated_ts
assert lstate.last_reported_timestamp == row.last_updated_ts
assert lstate.last_changed_timestamp == last_changed_ts
assert lstate.last_updated_timestamp == last_updated_ts
assert lstate.last_reported_timestamp == last_updated_ts
async def test_lazy_state_handles_same_last_updated_and_last_changed(
caplog: pytest.LogCaptureFixture,
) -> None:
async def test_lazy_state_handles_same_last_updated_and_last_changed() -> None:
"""Test that the LazyState handles same last_updated and last_changed."""
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
row = PropertyMock(
entity_id="sensor.valid",
state="off",
attributes='{"shared":true}',
last_updated_ts=now.timestamp(),
last_changed_ts=now.timestamp(),
last_reported_ts=None,
)
last_updated_ts = now.timestamp()
lstate = LazyState(
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
{},
None,
"sensor.valid",
"off",
last_updated_ts,
'{"shared":true}',
last_updated_ts,
None,
)
assert lstate.as_dict() == {
"attributes": {"shared": True},
@@ -383,9 +369,9 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(
"last_updated": "2021-06-12T03:04:01.000323+00:00",
"state": "off",
}
assert lstate.last_updated.timestamp() == row.last_updated_ts
assert lstate.last_changed.timestamp() == row.last_changed_ts
assert lstate.last_reported.timestamp() == row.last_updated_ts
assert lstate.last_updated.timestamp() == last_updated_ts
assert lstate.last_changed.timestamp() == last_updated_ts
assert lstate.last_reported.timestamp() == last_updated_ts
assert lstate.as_dict() == {
"attributes": {"shared": True},
"entity_id": "sensor.valid",
@@ -393,26 +379,25 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(
"last_updated": "2021-06-12T03:04:01.000323+00:00",
"state": "off",
}
assert lstate.last_changed_timestamp == row.last_changed_ts
assert lstate.last_updated_timestamp == row.last_updated_ts
assert lstate.last_reported_timestamp == row.last_updated_ts
assert lstate.last_changed_timestamp == last_updated_ts
assert lstate.last_updated_timestamp == last_updated_ts
assert lstate.last_reported_timestamp == last_updated_ts
async def test_lazy_state_handles_different_last_reported(
caplog: pytest.LogCaptureFixture,
) -> None:
async def test_lazy_state_handles_different_last_reported() -> None:
"""Test that the LazyState handles last_reported different from last_updated."""
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
row = PropertyMock(
entity_id="sensor.valid",
state="off",
attributes='{"shared":true}',
last_updated_ts=(now - timedelta(seconds=60)).timestamp(),
last_reported_ts=now.timestamp(),
last_changed_ts=(now - timedelta(seconds=60)).timestamp(),
)
last_reported_ts = now.timestamp()
last_updated_ts = (now - timedelta(seconds=60)).timestamp()
lstate = LazyState(
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
{},
None,
"sensor.valid",
"off",
last_updated_ts,
'{"shared":true}',
last_updated_ts,
last_reported_ts,
)
assert lstate.as_dict() == {
"attributes": {"shared": True},
@@ -421,9 +406,9 @@ async def test_lazy_state_handles_different_last_reported(
"last_updated": "2021-06-12T03:03:01.000323+00:00",
"state": "off",
}
assert lstate.last_updated.timestamp() == row.last_updated_ts
assert lstate.last_changed.timestamp() == row.last_changed_ts
assert lstate.last_reported.timestamp() == row.last_reported_ts
assert lstate.last_changed_timestamp == row.last_changed_ts
assert lstate.last_updated_timestamp == row.last_updated_ts
assert lstate.last_reported_timestamp == row.last_reported_ts
assert lstate.last_updated.timestamp() == last_updated_ts
assert lstate.last_changed.timestamp() == last_updated_ts
assert lstate.last_reported.timestamp() == last_reported_ts
assert lstate.last_changed_timestamp == last_updated_ts
assert lstate.last_updated_timestamp == last_updated_ts
assert lstate.last_reported_timestamp == last_reported_ts