Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7fd16b1
Upgrade SQLAlchemy to 2.0.0
emontnemery Jan 23, 2023
ea43b82
Add sqlalchemy[mypy] package
emontnemery Jan 23, 2023
16c84b0
Enable the plugin
emontnemery Jan 23, 2023
5d74e95
typing fixes
bdraco Jan 23, 2023
baa6cc4
make time_fired_ts,origin_idx nullable
bdraco Jan 24, 2023
6d2d337
make time_fired_ts,origin_idx nullable
bdraco Jan 24, 2023
3ccab8d
drop nullable
bdraco Jan 24, 2023
60f8e42
Correct docstrings
emontnemery Jan 26, 2023
2b53ce7
Fix last mypy errors in recorder
emontnemery Jan 26, 2023
da400c9
Fix typing of SQL sensor
emontnemery Jan 26, 2023
8184867
Fix typing of logbook
emontnemery Jan 26, 2023
c7fcc8e
Remove leftover debug code
emontnemery Jan 26, 2023
a22848d
Bump sqlalchemy to 2.0.0
emontnemery Jan 30, 2023
6e6072d
Update SQL sensor test
emontnemery Jan 30, 2023
0198c94
Adjust typing after rebase
emontnemery Jan 30, 2023
1e27a2d
Add workaround for sqlalchemy_utils
emontnemery Jan 30, 2023
01e5cb6
Fix override of sqlachemy_utils function
emontnemery Jan 30, 2023
b6e0269
Merge branch 'dev' into sql_alchemy_20
bdraco Jan 30, 2023
e7cb74a
drop sqlalchemy[mypy] since it breaks package constraints
bdraco Jan 30, 2023
01e0b23
Disable mypy plugin
emontnemery Jan 31, 2023
edee927
fix test to only check which entities are returned as we do not care …
bdraco Jan 31, 2023
1c7127d
fix test to only check which entities are returned as we do not care …
bdraco Jan 31, 2023
7fc14f4
Merge remote-tracking branch 'upstream/sql_alchemy_20' into sql_alche…
bdraco Jan 31, 2023
68e76f6
Merge branch 'dev' into sql_alchemy_20
bdraco Jan 31, 2023
24ef8e0
fix sql manifest
bdraco Jan 31, 2023
d07cc9a
Merge remote-tracking branch 'upstream/sql_alchemy_20' into sql_alche…
bdraco Jan 31, 2023
43bb538
bump to 2.0.1
bdraco Feb 1, 2023
f6be059
Merge branch 'dev' into sql_alchemy_20
bdraco Feb 2, 2023
9461aea
Merge branch 'dev' into sql_alchemy_20
bdraco Feb 4, 2023
69f35f2
Merge branch 'dev' into sql_alchemy_20
bdraco Feb 7, 2023
7506331
bump to 2.0.2
bdraco Feb 7, 2023
f254631
Merge branch 'dev' into sql_alchemy_20
bdraco Feb 7, 2023
6d3a752
make sure sqlalchemy_utils patcher works for postgresql as well
bdraco Feb 7, 2023
0e5ebf6
Merge branch 'dev' into sql_alchemy_20
bdraco Feb 7, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/logbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ class EventAsRow:


@callback
def async_event_to_row(event: Event) -> EventAsRow | None:
def async_event_to_row(event: Event) -> EventAsRow:
"""Convert an event to a row."""
if event.event_type != EVENT_STATE_CHANGED:
return EventAsRow(
Expand Down
24 changes: 12 additions & 12 deletions homeassistant/components/logbook/processor.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""Event parser and human readable log generator."""
from __future__ import annotations

from collections.abc import Callable, Generator
from collections.abc import Callable, Generator, Sequence
from contextlib import suppress
from dataclasses import dataclass
from datetime import datetime as dt
from typing import Any

from sqlalchemy.engine import Result
from sqlalchemy.engine.row import Row
from sqlalchemy.orm.query import Query

from homeassistant.components.recorder.filters import Filters
from homeassistant.components.recorder.models import (
Expand Down Expand Up @@ -70,7 +70,7 @@ class LogbookRun:
event_cache: EventCache
entity_name_cache: EntityNameCache
include_entity_name: bool
format_time: Callable[[Row], Any]
format_time: Callable[[Row | EventAsRow], Any]


class EventProcessor:
Expand Down Expand Up @@ -133,13 +133,13 @@ def get_events(
) -> list[dict[str, Any]]:
"""Get events for a period of time."""

def yield_rows(query: Query) -> Generator[Row, None, None]:
def yield_rows(result: Result) -> Sequence[Row] | Result:
"""Yield rows from the database."""
# end_day - start_day intentionally checks .days and not .total_seconds()
# since we don't want to switch over to buffered if they go
# over one day by a few hours since the UI makes it so easy to do that.
if self.limited_select or (end_day - start_day).days <= 1:
return query.all() # type: ignore[no-any-return]
return result.all()
# Only buffer rows to reduce memory pressure
# if we expect the result set is going to be very large.
# What is considered very large is going to differ
Expand All @@ -149,7 +149,7 @@ def yield_rows(query: Query) -> Generator[Row, None, None]:
# even and RPi3 that number seems higher in testing
# so we don't switch over until we request > 1 day+ of data.
#
return query.yield_per(1024) # type: ignore[no-any-return]
return result.yield_per(1024)

stmt = statement_for_request(
start_day,
Expand All @@ -164,12 +164,12 @@ def yield_rows(query: Query) -> Generator[Row, None, None]:
return self.humanify(yield_rows(session.execute(stmt)))

def humanify(
self, row_generator: Generator[Row | EventAsRow, None, None]
self, rows: Generator[EventAsRow, None, None] | Sequence[Row] | Result
) -> list[dict[str, str]]:
"""Humanify rows."""
return list(
_humanify(
row_generator,
rows,
self.ent_reg,
self.logbook_run,
self.context_augmenter,
Expand All @@ -178,7 +178,7 @@ def humanify(


def _humanify(
rows: Generator[Row | EventAsRow, None, None],
rows: Generator[EventAsRow, None, None] | Sequence[Row] | Result,
ent_reg: er.EntityRegistry,
logbook_run: LogbookRun,
context_augmenter: ContextAugmenter,
Expand Down Expand Up @@ -263,7 +263,7 @@ def __init__(self, hass: HomeAssistant) -> None:
self._memorize_new = True
self._lookup: dict[str | None, Row | EventAsRow | None] = {None: None}

def memorize(self, row: Row) -> str | None:
def memorize(self, row: Row | EventAsRow) -> str | None:
"""Memorize a context from the database."""
if self._memorize_new:
context_id: str = row.context_id
Expand All @@ -276,7 +276,7 @@ def clear(self) -> None:
self._lookup.clear()
self._memorize_new = False

def get(self, context_id: str) -> Row | None:
def get(self, context_id: str) -> Row | EventAsRow | None:
"""Get the context origin."""
return self._lookup.get(context_id)

Expand All @@ -294,7 +294,7 @@ def __init__(self, logbook_run: LogbookRun) -> None:

def _get_context_row(
self, context_id: str | None, row: Row | EventAsRow
) -> Row | EventAsRow:
) -> Row | EventAsRow | None:
"""Get the context row from the id or row context."""
if context_id:
return self.context_lookup.get(context_id)
Expand Down
35 changes: 24 additions & 11 deletions homeassistant/components/logbook/queries/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from __future__ import annotations

from sqlalchemy import lambda_stmt
from sqlalchemy.orm import Query
from sqlalchemy.sql.elements import ClauseList
from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.sql.lambdas import StatementLambdaElement
from sqlalchemy.sql.selectable import Select

from homeassistant.components.recorder.db_schema import (
LAST_UPDATED_INDEX_TS,
Expand All @@ -24,8 +24,8 @@ def all_stmt(
start_day: float,
end_day: float,
event_types: tuple[str, ...],
states_entity_filter: ClauseList | None = None,
events_entity_filter: ClauseList | None = None,
states_entity_filter: ColumnElement | None = None,
events_entity_filter: ColumnElement | None = None,
context_id: str | None = None,
) -> StatementLambdaElement:
"""Generate a logbook query for all entities."""
Expand All @@ -37,16 +37,29 @@ def all_stmt(
# are gone from the database remove the
# _legacy_select_events_context_id()
stmt += lambda s: s.where(Events.context_id == context_id).union_all(
_states_query_for_context_id(start_day, end_day, context_id),
legacy_select_events_context_id(start_day, end_day, context_id),
_states_query_for_context_id(
start_day,
end_day,
# https://github.com/python/mypy/issues/2608
context_id, # type:ignore[arg-type]
),
legacy_select_events_context_id(
start_day,
end_day,
# https://github.com/python/mypy/issues/2608
context_id, # type:ignore[arg-type]
),
)
else:
if events_entity_filter is not None:
stmt += lambda s: s.where(events_entity_filter)

if states_entity_filter is not None:
stmt += lambda s: s.union_all(
_states_query_for_all(start_day, end_day).where(states_entity_filter)
_states_query_for_all(start_day, end_day).where(
# https://github.com/python/mypy/issues/2608
states_entity_filter # type:ignore[arg-type]
)
)
else:
stmt += lambda s: s.union_all(_states_query_for_all(start_day, end_day))
Expand All @@ -55,20 +68,20 @@ def all_stmt(
return stmt


def _states_query_for_all(start_day: float, end_day: float) -> Query:
def _states_query_for_all(start_day: float, end_day: float) -> Select:
return apply_states_filters(_apply_all_hints(select_states()), start_day, end_day)


def _apply_all_hints(query: Query) -> Query:
def _apply_all_hints(sel: Select) -> Select:
"""Force mysql to use the right index on large selects."""
return query.with_hint(
return sel.with_hint(
States, f"FORCE INDEX ({LAST_UPDATED_INDEX_TS})", dialect_name="mysql"
)


def _states_query_for_context_id(
start_day: float, end_day: float, context_id: str
) -> Query:
) -> Select:
return apply_states_filters(select_states(), start_day, end_day).where(
States.context_id == context_id
)
37 changes: 18 additions & 19 deletions homeassistant/components/logbook/queries/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

import sqlalchemy
from sqlalchemy import select
from sqlalchemy.orm import Query
from sqlalchemy.sql.elements import ClauseList
from sqlalchemy.sql.elements import BooleanClauseList, ColumnElement
from sqlalchemy.sql.expression import literal
from sqlalchemy.sql.selectable import Select

Expand Down Expand Up @@ -69,7 +68,7 @@
literal(value=None, type_=sqlalchemy.String).label("old_format_icon"),
)

EVENT_COLUMNS_FOR_STATE_SELECT = [
EVENT_COLUMNS_FOR_STATE_SELECT = (
literal(value=None, type_=sqlalchemy.Text).label("event_id"),
# We use PSEUDO_EVENT_STATE_CHANGED aka None for
# state_changed events since it takes up less
Expand All @@ -84,7 +83,7 @@
States.context_user_id.label("context_user_id"),
States.context_parent_id.label("context_parent_id"),
literal(value=None, type_=sqlalchemy.Text).label("shared_data"),
]
)

EMPTY_STATE_COLUMNS = (
literal(value=0, type_=sqlalchemy.Integer).label("state_id"),
Expand All @@ -103,8 +102,8 @@

# Virtual column to tell logbook if it should avoid processing
# the event as its only used to link contexts
CONTEXT_ONLY = literal("1").label("context_only")
NOT_CONTEXT_ONLY = literal(None).label("context_only")
CONTEXT_ONLY = literal(value="1", type_=sqlalchemy.String).label("context_only")
NOT_CONTEXT_ONLY = literal(value=None, type_=sqlalchemy.String).label("context_only")


def select_events_context_id_subquery(
Expand Down Expand Up @@ -188,15 +187,15 @@ def legacy_select_events_context_id(
)


def apply_states_filters(query: Query, start_day: float, end_day: float) -> Query:
def apply_states_filters(sel: Select, start_day: float, end_day: float) -> Select:
"""Filter states by time range.

Filters states that do not have an old state or new state (added / removed)
Filters states that are in a continuous domain with a UOM.
Filters states that do not have matching last_updated_ts and last_changed_ts.
"""
return (
query.filter(
sel.filter(
(States.last_updated_ts > start_day) & (States.last_updated_ts < end_day)
)
.outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id))
Expand All @@ -212,18 +211,18 @@ def apply_states_filters(query: Query, start_day: float, end_day: float) -> Quer
)


def _missing_state_matcher() -> sqlalchemy.and_:
def _missing_state_matcher() -> ColumnElement[bool]:
# The below removes state change events that do not have
# and old_state or the old_state is missing (newly added entities)
# or the new_state is missing (removed entities)
return sqlalchemy.and_(
OLD_STATE.state_id.isnot(None),
OLD_STATE.state_id.is_not(None),
(States.state != OLD_STATE.state),
States.state.isnot(None),
States.state.is_not(None),
)


def _not_continuous_entity_matcher() -> sqlalchemy.or_:
def _not_continuous_entity_matcher() -> ColumnElement[bool]:
"""Match non continuous entities."""
return sqlalchemy.or_(
# First exclude domains that may be continuous
Expand All @@ -236,7 +235,7 @@ def _not_continuous_entity_matcher() -> sqlalchemy.or_:
)


def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_:
def _not_possible_continuous_domain_matcher() -> ColumnElement[bool]:
"""Match not continuous domains.

This matches domain that are always considered continuous
Expand All @@ -254,7 +253,7 @@ def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_:
).self_group()


def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_:
def _conditionally_continuous_domain_matcher() -> ColumnElement[bool]:
"""Match conditionally continuous domains.

This matches domain that are only considered
Expand All @@ -268,22 +267,22 @@ def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_:
).self_group()


def _not_uom_attributes_matcher() -> ClauseList:
def _not_uom_attributes_matcher() -> BooleanClauseList:
"""Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql."""
return ~StateAttributes.shared_attrs.like(
UNIT_OF_MEASUREMENT_JSON_LIKE
) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE)


def apply_states_context_hints(query: Query) -> Query:
def apply_states_context_hints(sel: Select) -> Select:
"""Force mysql to use the right index on large context_id selects."""
return query.with_hint(
return sel.with_hint(
States, f"FORCE INDEX ({STATES_CONTEXT_ID_INDEX})", dialect_name="mysql"
)


def apply_events_context_hints(query: Query) -> Query:
def apply_events_context_hints(sel: Select) -> Select:
"""Force mysql to use the right index on large context_id selects."""
return query.with_hint(
return sel.with_hint(
Events, f"FORCE INDEX ({EVENTS_CONTEXT_ID_INDEX})", dialect_name="mysql"
)
13 changes: 6 additions & 7 deletions homeassistant/components/logbook/queries/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@

import sqlalchemy
from sqlalchemy import lambda_stmt, select
from sqlalchemy.orm import Query
from sqlalchemy.sql.elements import ClauseList
from sqlalchemy.sql.elements import BooleanClauseList
from sqlalchemy.sql.lambdas import StatementLambdaElement
from sqlalchemy.sql.selectable import CTE, CompoundSelect
from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select

from homeassistant.components.recorder.db_schema import (
DEVICE_ID_IN_EVENT,
Expand All @@ -32,7 +31,7 @@ def _select_device_id_context_ids_sub_query(
end_day: float,
event_types: tuple[str, ...],
json_quotable_device_ids: list[str],
) -> CompoundSelect:
) -> Select:
"""Generate a subquery to find context ids for multiple devices."""
inner = select_events_context_id_subquery(start_day, end_day, event_types).where(
apply_event_device_id_matchers(json_quotable_device_ids)
Expand All @@ -41,7 +40,7 @@ def _select_device_id_context_ids_sub_query(


def _apply_devices_context_union(
query: Query,
sel: Select,
start_day: float,
end_day: float,
event_types: tuple[str, ...],
Expand All @@ -54,7 +53,7 @@ def _apply_devices_context_union(
event_types,
json_quotable_device_ids,
).cte()
return query.union_all(
return sel.union_all(
apply_events_context_hints(
select_events_context_only()
.select_from(devices_cte)
Expand Down Expand Up @@ -91,7 +90,7 @@ def devices_stmt(

def apply_event_device_id_matchers(
json_quotable_device_ids: Iterable[str],
) -> ClauseList:
) -> BooleanClauseList:
"""Create matchers for the device_ids in the event_data."""
return DEVICE_ID_IN_EVENT.is_not(None) & sqlalchemy.cast(
DEVICE_ID_IN_EVENT, sqlalchemy.Text()
Expand Down
Loading