1 """Queries for logbook."""
3 from __future__
import annotations
5 from typing
import Final
8 from sqlalchemy
import select
9 from sqlalchemy.sql.elements
import BooleanClauseList, ColumnElement
10 from sqlalchemy.sql.expression
import literal
11 from sqlalchemy.sql.selectable
import Select
14 EVENTS_CONTEXT_ID_BIN_INDEX,
15 OLD_FORMAT_ATTRS_JSON,
18 SHARED_DATA_OR_LEGACY_EVENT_DATA,
19 STATES_CONTEXT_ID_BIN_INDEX,
29 from ..const
import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS
33 CONDITIONALLY_CONTINUOUS_DOMAINS
38 UNIT_OF_MEASUREMENT_JSON =
'"unit_of_measurement":'
39 UNIT_OF_MEASUREMENT_JSON_LIKE = f
"%{UNIT_OF_MEASUREMENT_JSON}%"
41 ICON_OR_OLD_FORMAT_ICON_JSON = sqlalchemy.case(
42 (SHARED_ATTRS_JSON[
"icon"].is_(
None), OLD_FORMAT_ATTRS_JSON[
"icon"].as_string()),
43 else_=SHARED_ATTRS_JSON[
"icon"].as_string(),
46 PSEUDO_EVENT_STATE_CHANGED: Final =
None
55 Events.event_id.label(
"row_id"),
56 EventTypes.event_type.label(
"event_type"),
57 SHARED_DATA_OR_LEGACY_EVENT_DATA,
58 Events.time_fired_ts.label(
"time_fired_ts"),
59 Events.context_id_bin.label(
"context_id_bin"),
60 Events.context_user_id_bin.label(
"context_user_id_bin"),
61 Events.context_parent_id_bin.label(
"context_parent_id_bin"),
65 States.state.label(
"state"),
66 StatesMeta.entity_id.label(
"entity_id"),
67 ICON_OR_OLD_FORMAT_ICON_JSON,
70 STATE_CONTEXT_ONLY_COLUMNS = (
71 States.state.label(
"state"),
72 StatesMeta.entity_id.label(
"entity_id"),
73 literal(value=
None, type_=sqlalchemy.String).label(
"icon"),
76 EVENT_COLUMNS_FOR_STATE_SELECT = (
77 States.state_id.label(
"row_id"),
82 literal(value=PSEUDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label(
85 literal(value=
None, type_=sqlalchemy.Text).label(
"event_data"),
86 States.last_updated_ts.label(
"time_fired_ts"),
87 States.context_id_bin.label(
"context_id_bin"),
88 States.context_user_id_bin.label(
"context_user_id_bin"),
89 States.context_parent_id_bin.label(
"context_parent_id_bin"),
92 EMPTY_STATE_COLUMNS = (
93 literal(value=
None, type_=sqlalchemy.String).label(
"state"),
94 literal(value=
None, type_=sqlalchemy.String).label(
"entity_id"),
95 literal(value=
None, type_=sqlalchemy.String).label(
"icon"),
99 EVENT_ROWS_NO_STATES = (
101 *EMPTY_STATE_COLUMNS,
106 CONTEXT_ONLY = literal(value=
"1", type_=sqlalchemy.String).label(
"context_only")
107 NOT_CONTEXT_ONLY = literal(value=
None, type_=sqlalchemy.String).label(
"context_only")
113 event_type_ids: tuple[int, ...],
115 """Generate the select for a context_id subquery."""
117 select(Events.context_id_bin)
118 .where((Events.time_fired_ts > start_day) & (Events.time_fired_ts < end_day))
119 .where(Events.event_type_id.in_(event_type_ids))
120 .outerjoin(EventTypes, (Events.event_type_id == EventTypes.event_type_id))
121 .outerjoin(EventData, (Events.data_id == EventData.data_id))
126 """Generate an events query that mark them as for context_only.
128 By marking them as context_only we know they are only for
129 linking context ids and we can avoid processing them.
131 return select(*EVENT_ROWS_NO_STATES, CONTEXT_ONLY)
135 """Generate an states query that mark them as for context_only.
137 By marking them as context_only we know they are only for
138 linking context ids and we can avoid processing them.
141 *EVENT_COLUMNS_FOR_STATE_SELECT, *STATE_CONTEXT_ONLY_COLUMNS, CONTEXT_ONLY
146 start_day: float, end_day: float, event_type_ids: tuple[int, ...]
148 """Generate an events select that does not join states."""
150 select(*EVENT_ROWS_NO_STATES, NOT_CONTEXT_ONLY)
151 .where((Events.time_fired_ts > start_day) & (Events.time_fired_ts < end_day))
152 .where(Events.event_type_id.in_(event_type_ids))
153 .outerjoin(EventTypes, (Events.event_type_id == EventTypes.event_type_id))
154 .outerjoin(EventData, (Events.data_id == EventData.data_id))
159 """Generate a states select that formats the states table as event rows."""
161 *EVENT_COLUMNS_FOR_STATE_SELECT,
168 """Filter states by time range.
170 Filters states that do not have an old state or new state (added / removed)
171 Filters states that are in a continuous domain with a UOM.
172 Filters states that do not have matching last_updated_ts and last_changed_ts.
176 (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day)
178 .outerjoin(OLD_STATE, (States.old_state_id == OLD_STATE.state_id))
182 (States.last_updated_ts == States.last_changed_ts)
183 | States.last_changed_ts.is_(
None)
186 StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
188 .outerjoin(StatesMeta, (States.metadata_id == StatesMeta.metadata_id))
196 return sqlalchemy.and_(
197 OLD_STATE.state_id.is_not(
None),
198 (States.state != OLD_STATE.state),
199 States.state.is_not(
None),
204 """Match non continuous entities."""
205 return sqlalchemy.or_(
217 """Match not continuous domains.
219 This matches domain that are always considered continuous
220 and domains that are conditionally (if they have a UOM)
223 return sqlalchemy.and_(
225 ~StatesMeta.entity_id.like(entity_domain)
226 for entity_domain
in (
227 *ALWAYS_CONTINUOUS_ENTITY_ID_LIKE,
228 *CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE,
235 """Match conditionally continuous domains.
237 This matches domain that are only considered
238 continuous if a UOM is set.
240 return sqlalchemy.or_(
242 StatesMeta.entity_id.like(entity_domain)
243 for entity_domain
in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE
249 """Prefilter ATTR_UNIT_OF_MEASUREMENT as its much faster in sql."""
250 return ~StateAttributes.shared_attrs.like(
251 UNIT_OF_MEASUREMENT_JSON_LIKE
252 ) | ~States.attributes.like(UNIT_OF_MEASUREMENT_JSON_LIKE)
256 """Force mysql to use the right index on large context_id selects."""
257 return sel.with_hint(
258 States, f
"FORCE INDEX ({STATES_CONTEXT_ID_BIN_INDEX})", dialect_name=
"mysql"
260 States, f
"FORCE INDEX ({STATES_CONTEXT_ID_BIN_INDEX})", dialect_name=
"mariadb"
265 """Force mysql to use the right index on large context_id selects."""
266 return sel.with_hint(
267 Events, f
"FORCE INDEX ({EVENTS_CONTEXT_ID_BIN_INDEX})", dialect_name=
"mysql"
269 Events, f
"FORCE INDEX ({EVENTS_CONTEXT_ID_BIN_INDEX})", dialect_name=
"mariadb"
Select apply_states_filters(Select sel, float start_day, float end_day)
Select select_states_context_only()
ColumnElement[bool] _conditionally_continuous_domain_matcher()
ColumnElement[bool] _not_continuous_entity_matcher()
BooleanClauseList _not_uom_attributes_matcher()
Select select_events_without_states(float start_day, float end_day, tuple[int,...] event_type_ids)
Select select_events_context_id_subquery(float start_day, float end_day, tuple[int,...] event_type_ids)
Select apply_states_context_hints(Select sel)
Select apply_events_context_hints(Select sel)
ColumnElement[bool] _not_possible_continuous_domain_matcher()
ColumnElement[bool] _missing_state_matcher()
Select select_events_context_only()
list[str] like_domain_matchers(Iterable[str] domains)