-
Notifications
You must be signed in to change notification settings - Fork 1.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: added support for daily and weekly active user aggregations #18101
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
73002a3
Added support for daily and weekly active user aggregations
Gilbert09 b4c2be2
Removed unused join
Gilbert09 84d4019
Merge branch 'master' into feat/trends-weekly-active
Gilbert09 2f704ac
Fixed tests and applied Sample placeholders
Gilbert09 5e9f06e
Fixed formula running on an empty string
Gilbert09 896d0ff
Updated the name of QueryModifier
Gilbert09 d8b0c70
Comments and fixed name casing
Gilbert09 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
149 changes: 149 additions & 0 deletions
149
posthog/hogql_queries/insights/trends/aggregation_operations.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
from typing import List | ||
from posthog.hogql import ast | ||
from posthog.hogql.parser import parse_expr, parse_select | ||
from posthog.hogql_queries.utils.query_date_range import QueryDateRange | ||
from posthog.schema import ActionsNode, EventsNode | ||
|
||
|
||
class QueryAlternator: | ||
"""Allows query_builder to modify the query without having to expost the whole AST interface""" | ||
|
||
_query: ast.SelectQuery | ||
_selects: List[ast.Expr] | ||
_group_bys: List[ast.Expr] | ||
_select_from: ast.JoinExpr | None | ||
|
||
def __init__(self, query: ast.SelectQuery): | ||
self._query = query | ||
self._selects = [] | ||
self._group_bys = [] | ||
self._select_from = None | ||
|
||
def build(self) -> ast.SelectQuery: | ||
if len(self._selects) > 0: | ||
self._query.select.extend(self._selects) | ||
|
||
if len(self._group_bys) > 0: | ||
if self._query.group_by is None: | ||
self._query.group_by = self._group_bys | ||
else: | ||
self._query.group_by.extend(self._group_bys) | ||
|
||
if self._select_from is not None: | ||
self._query.select_from = self._select_from | ||
|
||
return self._query | ||
|
||
def append_select(self, expr: ast.Expr) -> None: | ||
self._selects.append(expr) | ||
|
||
def append_group_by(self, expr: ast.Expr) -> None: | ||
self._group_bys.append(expr) | ||
|
||
def replace_select_from(self, join_expr: ast.JoinExpr) -> None: | ||
self._select_from = join_expr | ||
|
||
|
||
class AggregationOperations: | ||
series: EventsNode | ActionsNode | ||
query_date_range: QueryDateRange | ||
|
||
def __init__(self, series: str, query_date_range: QueryDateRange) -> None: | ||
self.series = series | ||
self.query_date_range = query_date_range | ||
|
||
def select_aggregation(self) -> ast.Expr: | ||
if self.series.math == "hogql": | ||
return parse_expr(self.series.math_hogql) | ||
elif self.series.math == "total": | ||
return parse_expr("count(e.uuid)") | ||
elif self.series.math == "dau": | ||
return parse_expr("count(DISTINCT e.person_id)") | ||
elif self.series.math == "weekly_active": | ||
return ast.Field(chain=["counts"]) | ||
|
||
return parse_expr("count(e.uuid)") | ||
|
||
def requires_query_orchestration(self) -> bool: | ||
return self.series.math == "weekly_active" | ||
|
||
def _parent_select_query(self, inner_query: ast.SelectQuery) -> ast.SelectQuery: | ||
return parse_select( | ||
""" | ||
SELECT | ||
counts AS total, | ||
dateTrunc({interval}, timestamp) AS day_start | ||
FROM {inner_query} | ||
WHERE timestamp >= {date_from} AND timestamp <= {date_to} | ||
""", | ||
placeholders={ | ||
**self.query_date_range.to_placeholders(), | ||
"inner_query": inner_query, | ||
}, | ||
) | ||
|
||
def _inner_select_query(self, cross_join_select_query: ast.SelectQuery) -> ast.SelectQuery: | ||
return parse_select( | ||
""" | ||
SELECT | ||
d.timestamp, | ||
COUNT(DISTINCT actor_id) AS counts | ||
FROM ( | ||
SELECT | ||
toStartOfDay({date_to}) - toIntervalDay(number) AS timestamp | ||
FROM | ||
numbers(dateDiff('day', toStartOfDay({date_from} - INTERVAL 7 DAY), {date_to})) | ||
) d | ||
CROSS JOIN {cross_join_select_query} e | ||
WHERE | ||
e.timestamp <= d.timestamp + INTERVAL 1 DAY AND | ||
e.timestamp > d.timestamp - INTERVAL 6 DAY | ||
GROUP BY d.timestamp | ||
ORDER BY d.timestamp | ||
""", | ||
placeholders={ | ||
**self.query_date_range.to_placeholders(), | ||
"cross_join_select_query": cross_join_select_query, | ||
}, | ||
) | ||
|
||
def _events_query(self, events_where_clause: ast.Expr, sample_value: ast.RatioExpr) -> ast.SelectQuery: | ||
return parse_select( | ||
""" | ||
SELECT | ||
timestamp as timestamp, | ||
e.person_id AS actor_id | ||
FROM | ||
events e | ||
SAMPLE {sample} | ||
WHERE {events_where_clause} | ||
GROUP BY | ||
timestamp, | ||
actor_id | ||
""", | ||
placeholders={"events_where_clause": events_where_clause, "sample": sample_value}, | ||
) | ||
|
||
def get_query_orchestrator(self, events_where_clause: ast.Expr, sample_value: str): | ||
events_query = self._events_query(events_where_clause, sample_value) | ||
inner_select = self._inner_select_query(events_query) | ||
parent_select = self._parent_select_query(inner_select) | ||
|
||
class QueryOrchestrator: | ||
events_query_builder: QueryAlternator | ||
inner_select_query_builder: QueryAlternator | ||
parent_select_query_builder: QueryAlternator | ||
|
||
def __init__(self): | ||
self.events_query_builder = QueryAlternator(events_query) | ||
self.inner_select_query_builder = QueryAlternator(inner_select) | ||
self.parent_select_query_builder = QueryAlternator(parent_select) | ||
|
||
def build(self): | ||
self.events_query_builder.build() | ||
self.inner_select_query_builder.build() | ||
self.parent_select_query_builder.build() | ||
|
||
return parent_select | ||
|
||
return QueryOrchestrator() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
posthog/hogql_queries/insights/trends/test/test_aggregation_operations.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
from typing import cast | ||
from posthog.hogql import ast | ||
from posthog.hogql.parser import parse_select | ||
from posthog.hogql_queries.insights.trends.aggregation_operations import QueryAlternator | ||
|
||
|
||
class TestQueryAlternator: | ||
def test_select(self): | ||
query = parse_select("SELECT event from events") | ||
|
||
query_modifier = QueryAlternator(query) | ||
query_modifier.append_select(ast.Field(chain=["test"])) | ||
query_modifier.build() | ||
|
||
assert len(query.select) == 2 | ||
assert cast(ast.Field, query.select[1]).chain == ["test"] | ||
|
||
def test_group_no_pre_existing(self): | ||
query = parse_select("SELECT event from events") | ||
|
||
query_modifier = QueryAlternator(query) | ||
query_modifier.append_group_by(ast.Field(chain=["event"])) | ||
query_modifier.build() | ||
|
||
assert len(query.group_by) == 1 | ||
assert cast(ast.Field, query.group_by[0]).chain == ["event"] | ||
|
||
def test_group_with_pre_existing(self): | ||
query = parse_select("SELECT event from events GROUP BY uuid") | ||
|
||
query_modifier = QueryAlternator(query) | ||
query_modifier.append_group_by(ast.Field(chain=["event"])) | ||
query_modifier.build() | ||
|
||
assert len(query.group_by) == 2 | ||
assert cast(ast.Field, query.group_by[0]).chain == ["uuid"] | ||
assert cast(ast.Field, query.group_by[1]).chain == ["event"] | ||
|
||
def test_replace_select_from(self): | ||
query = parse_select("SELECT event from events") | ||
|
||
query_modifier = QueryAlternator(query) | ||
query_modifier.replace_select_from(ast.JoinExpr(table=ast.Field(chain=["groups"]))) | ||
query_modifier.build() | ||
|
||
assert query.select_from.table.chain == ["groups"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be good to leave a line of prose for the future traveller, as to what this class does, without having to go through all the code.