forked from PostHog/posthog
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(queries): persons query source (PostHog#18060)
- Loading branch information
1 parent
697f38f
commit e6ffbef
Showing
11 changed files
with
343 additions
and
129 deletions.
There are no files selected for viewing
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
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
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
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
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,68 @@ | ||
from datetime import timedelta | ||
from typing import Dict, Optional, Any | ||
|
||
from posthog.clickhouse.client.connection import Workload | ||
from posthog.hogql import ast | ||
from posthog.hogql.filters import replace_filters | ||
from posthog.hogql.parser import parse_select | ||
from posthog.hogql.placeholders import find_placeholders | ||
from posthog.hogql.query import execute_hogql_query | ||
from posthog.hogql.timings import HogQLTimings | ||
from posthog.hogql_queries.query_runner import QueryRunner | ||
from posthog.models import Team | ||
from posthog.schema import HogQLQuery, HogQLQueryResponse | ||
|
||
|
||
class HogQLQueryRunner(QueryRunner): | ||
query: HogQLQuery | ||
query_type = HogQLQuery | ||
|
||
def __init__( | ||
self, | ||
query: HogQLQuery | Dict[str, Any], | ||
team: Team, | ||
timings: Optional[HogQLTimings] = None, | ||
in_export_context: Optional[bool] = False, | ||
): | ||
super().__init__(query, team, timings, in_export_context) | ||
if isinstance(query, HogQLQuery): | ||
self.query = query | ||
else: | ||
self.query = HogQLQuery.model_validate(query) | ||
|
||
def to_query(self) -> ast.SelectQuery: | ||
if self.timings is None: | ||
self.timings = HogQLTimings() | ||
values = ( | ||
{key: ast.Constant(value=value) for key, value in self.query.values.items()} if self.query.values else None | ||
) | ||
with self.timings.measure("parse_select"): | ||
parsed_select = parse_select(str(self.query.query), timings=self.timings, placeholders=values) | ||
|
||
if self.query.filters: | ||
with self.timings.measure("filters"): | ||
placeholders_in_query = find_placeholders(parsed_select) | ||
if "filters" in placeholders_in_query: | ||
parsed_select = replace_filters(parsed_select, self.query.filters, self.team) | ||
return parsed_select | ||
|
||
def to_persons_query(self) -> ast.SelectQuery: | ||
return self.to_query() | ||
|
||
def calculate(self) -> HogQLQueryResponse: | ||
return execute_hogql_query( | ||
query_type="HogQLQuery", | ||
query=self.to_query(), | ||
filters=self.query.filters, | ||
modifiers=self.query.modifiers, | ||
team=self.team, | ||
workload=Workload.ONLINE, | ||
timings=self.timings, | ||
in_export_context=self.in_export_context, | ||
) | ||
|
||
def _is_stale(self, cached_result_package): | ||
return True | ||
|
||
def _refresh_frequency(self): | ||
return timedelta(minutes=1) |
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
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
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
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,85 @@ | ||
from posthog.hogql import ast | ||
from posthog.hogql.visitor import clear_locations | ||
from posthog.hogql_queries.hogql_query_runner import HogQLQueryRunner | ||
from posthog.models.utils import UUIDT | ||
from posthog.schema import HogQLPropertyFilter, HogQLQuery, HogQLFilters | ||
from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_person, flush_persons_and_events, _create_event | ||
|
||
|
||
class TestHogQLQueryRunner(ClickhouseTestMixin, APIBaseTest): | ||
maxDiff = None | ||
random_uuid: str | ||
|
||
def _create_random_persons(self) -> str: | ||
random_uuid = str(UUIDT()) | ||
for index in range(10): | ||
_create_person( | ||
properties={ | ||
"email": f"jacob{index}@{random_uuid}.posthog.com", | ||
"name": f"Mr Jacob {random_uuid}", | ||
"random_uuid": random_uuid, | ||
"index": index, | ||
}, | ||
team=self.team, | ||
distinct_ids=[f"id-{random_uuid}-{index}"], | ||
is_identified=True, | ||
) | ||
_create_event(distinct_id=f"id-{random_uuid}-{index}", event=f"clicky-{index}", team=self.team) | ||
flush_persons_and_events() | ||
return random_uuid | ||
|
||
def _create_runner(self, query: HogQLQuery) -> HogQLQueryRunner: | ||
return HogQLQueryRunner(team=self.team, query=query) | ||
|
||
def setUp(self): | ||
super().setUp() | ||
self.random_uuid = self._create_random_persons() | ||
|
||
def test_default_hogql_query(self): | ||
runner = self._create_runner(HogQLQuery(query="select count(event) from events")) | ||
query = runner.to_query() | ||
query = clear_locations(query) | ||
expected = ast.SelectQuery( | ||
select=[ast.Call(name="count", args=[ast.Field(chain=["event"])])], | ||
select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), | ||
) | ||
self.assertEqual(clear_locations(query), expected) | ||
response = runner.calculate() | ||
self.assertEqual(response.results[0][0], 10) | ||
|
||
def test_hogql_query_filters(self): | ||
runner = self._create_runner( | ||
HogQLQuery( | ||
query="select count(event) from events where {filters}", | ||
filters=HogQLFilters(properties=[HogQLPropertyFilter(key="event='clicky-3'")]), | ||
) | ||
) | ||
query = runner.to_query() | ||
query = clear_locations(query) | ||
expected = ast.SelectQuery( | ||
select=[ast.Call(name="count", args=[ast.Field(chain=["event"])])], | ||
select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), | ||
where=ast.CompareOperation( | ||
left=ast.Field(chain=["event"]), op=ast.CompareOperationOp.Eq, right=ast.Constant(value="clicky-3") | ||
), | ||
) | ||
self.assertEqual(clear_locations(query), expected) | ||
response = runner.calculate() | ||
self.assertEqual(response.results[0][0], 1) | ||
|
||
def test_hogql_query_values(self): | ||
runner = self._create_runner( | ||
HogQLQuery(query="select count(event) from events where event={e}", values={"e": "clicky-3"}) | ||
) | ||
query = runner.to_query() | ||
query = clear_locations(query) | ||
expected = ast.SelectQuery( | ||
select=[ast.Call(name="count", args=[ast.Field(chain=["event"])])], | ||
select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), | ||
where=ast.CompareOperation( | ||
left=ast.Field(chain=["event"]), op=ast.CompareOperationOp.Eq, right=ast.Constant(value="clicky-3") | ||
), | ||
) | ||
self.assertEqual(clear_locations(query), expected) | ||
response = runner.calculate() | ||
self.assertEqual(response.results[0][0], 1) |
Oops, something went wrong.