Skip to content

Commit ae3bf47

Browse files
committed
fix: Support materialized views in groups revenue analytics join
1 parent 09c0ef1 commit ae3bf47

4 files changed

Lines changed: 238 additions & 1702 deletions

File tree

posthog/hogql/database/schema/groups_revenue_analytics.py

Lines changed: 8 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from collections import defaultdict
2-
from typing import Any, cast
32

43
from posthog.schema import DatabaseSchemaManagedViewTableKind
54

@@ -12,15 +11,13 @@
1211
LazyJoinToAdd,
1312
LazyTable,
1413
LazyTableToAdd,
15-
SavedQuery,
1614
StringDatabaseField,
1715
)
16+
from posthog.hogql.database.schema.util.revenue_analytics import get_table_kind, is_event_view
1817
from posthog.hogql.errors import ResolutionError
1918

2019
from posthog.models.exchange_rate.sql import EXCHANGE_RATE_DECIMAL_PRECISION
2120

22-
from products.revenue_analytics.backend.views.schemas import SCHEMAS as VIEW_SCHEMAS
23-
2421
ZERO_DECIMAL = ast.Call(
2522
name="toDecimal", args=[ast.Constant(value=0), ast.Constant(value=EXCHANGE_RATE_DECIMAL_PRECISION)]
2623
)
@@ -56,59 +53,19 @@ def join_with_groups_revenue_analytics_table(
5653
)
5754

5855

59-
def _base_view_args_from_saved_query(saved_query: SavedQuery) -> dict[str, Any]:
60-
return {
61-
"id": saved_query.id,
62-
"query": saved_query.query,
63-
"name": saved_query.name,
64-
"fields": saved_query.fields,
65-
"metadata": saved_query.metadata,
66-
# :KLUDGE: None of these properties below are great but it's all we can do to figure this one out for now
67-
# We'll be able to come up with a better solution we don't need to support the old managed views anymore
68-
"prefix": ".".join(saved_query.name.split(".")[:-1]),
69-
"source_id": None, # Not used so just ignore it
70-
"event_name": saved_query.name.split(".")[2] if "revenue_analytics.events" in saved_query.name else None,
71-
}
72-
73-
7456
def select_from_groups_revenue_analytics_table(context: HogQLContext) -> ast.SelectQuery | ast.SelectSetQuery:
75-
from products.revenue_analytics.backend.views import (
76-
RevenueAnalyticsBaseView,
77-
RevenueAnalyticsCustomerView,
78-
RevenueAnalyticsMRRView,
79-
RevenueAnalyticsRevenueItemView,
80-
)
81-
8257
if not context.database:
8358
return ast.SelectQuery.empty(columns=FIELDS)
8459

85-
customer_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_CUSTOMER]
86-
mrr_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_MRR]
87-
revenue_item_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_REVENUE_ITEM]
88-
8960
# Get all customer/mrr/revenue item tuples from the existing views making sure we ignore `all`
9061
# since the `group` join is in the child view
91-
all_views = defaultdict[str, dict[DatabaseSchemaManagedViewTableKind, RevenueAnalyticsBaseView]](defaultdict)
62+
all_views = defaultdict[str, dict](defaultdict)
9263
for view_name in context.database.get_view_names():
93-
view = cast(SavedQuery | RevenueAnalyticsBaseView, context.database.get_table(view_name))
94-
prefix = ".".join(view_name.split(".")[:-1])
95-
96-
# Might need to convert to RevenueAnalyticsBaseView from a SavedQuery if the FF is enabled
97-
# Soon we'll be able to remove all of this and handle them all using the `SavedQuery` logic directly
98-
if view_name.endswith(customer_schema.source_suffix) or view_name.endswith(customer_schema.events_suffix):
99-
if not isinstance(view, RevenueAnalyticsBaseView):
100-
view = RevenueAnalyticsCustomerView(**_base_view_args_from_saved_query(view))
101-
all_views[prefix][DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_CUSTOMER] = view
102-
elif view_name.endswith(revenue_item_schema.source_suffix) or view_name.endswith(
103-
revenue_item_schema.events_suffix
104-
):
105-
if not isinstance(view, RevenueAnalyticsBaseView):
106-
view = RevenueAnalyticsRevenueItemView(**_base_view_args_from_saved_query(view))
107-
all_views[prefix][DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_REVENUE_ITEM] = view
108-
elif view_name.endswith(mrr_schema.source_suffix) or view_name.endswith(mrr_schema.events_suffix):
109-
if not isinstance(view, RevenueAnalyticsBaseView):
110-
view = RevenueAnalyticsMRRView(**_base_view_args_from_saved_query(view))
111-
all_views[prefix][DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_MRR] = view
64+
table_kind = get_table_kind(view_name)
65+
if table_kind is not None:
66+
view = context.database.get_table(view_name)
67+
prefix = ".".join(view_name.split(".")[:-1])
68+
all_views[prefix][table_kind] = view
11269

11370
# Iterate over all possible view tuples and figure out which queries we can add to the set
11471
queries = []
@@ -121,7 +78,7 @@ def select_from_groups_revenue_analytics_table(context: HogQLContext) -> ast.Sel
12178
if customer_view is None or revenue_item_view is None or mrr_view is None:
12279
continue
12380

124-
if customer_view.is_event_view():
81+
if is_event_view(customer_view.name):
12582
# For events, group_keys are on each event (group_0_key through group_4_key)
12683
# We aggregate by group_key directly from each source, then FULL OUTER JOIN
12784
# since there's no base entity table to start from

posthog/hogql/database/schema/persons_revenue_analytics.py

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from collections import defaultdict
2-
from typing import Literal
32

43
from posthog.schema import DatabaseSchemaManagedViewTableKind
54

@@ -14,12 +13,11 @@
1413
LazyTableToAdd,
1514
StringDatabaseField,
1615
)
16+
from posthog.hogql.database.schema.util.revenue_analytics import get_table_kind, is_event_view
1717
from posthog.hogql.errors import ResolutionError
1818

1919
from posthog.models.exchange_rate.sql import EXCHANGE_RATE_DECIMAL_PRECISION
2020

21-
from products.revenue_analytics.backend.views.schemas import SCHEMAS as VIEW_SCHEMAS
22-
2321
ZERO_DECIMAL = ast.Call(
2422
name="toDecimal", args=[ast.Constant(value=0), ast.Constant(value=EXCHANGE_RATE_DECIMAL_PRECISION)]
2523
)
@@ -42,7 +40,7 @@ def _select_from_persons_revenue_analytics_table(context: HogQLContext) -> ast.S
4240
# since the `persons` join is in the child view
4341
all_views = defaultdict[str, dict](defaultdict)
4442
for view_name in context.database.get_view_names():
45-
table_kind = _get_table_kind(view_name)
43+
table_kind = get_table_kind(view_name)
4644
if table_kind is not None:
4745
view = context.database.get_table(view_name)
4846
prefix = ".".join(view_name.split(".")[:-1])
@@ -62,7 +60,7 @@ def _select_from_persons_revenue_analytics_table(context: HogQLContext) -> ast.S
6260
# If we're working with event views, we can use the customer's id field directly
6361
# Otherwise, we need to join with the persons table by checking whether it exists
6462
person_id_chain: list[str | int] | None = None
65-
if _is_event_view(customer_view.name):
63+
if is_event_view(customer_view.name):
6664
person_id_chain = [RevenueAnalyticsCustomerView.get_generic_view_alias(), "id"]
6765
else:
6866
persons_lazy_join = customer_view.fields.get("persons")
@@ -159,53 +157,6 @@ def _select_from_persons_revenue_analytics_table(context: HogQLContext) -> ast.S
159157
return ast.SelectSetQuery.create_from_queries(queries, set_operator="UNION ALL")
160158

161159

162-
def _get_table_kind(
163-
view_name: str,
164-
) -> (
165-
Literal[
166-
DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_CUSTOMER,
167-
DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_MRR,
168-
DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_REVENUE_ITEM,
169-
]
170-
| None
171-
):
172-
if _is_customer_schema(view_name=view_name):
173-
return DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_CUSTOMER
174-
175-
if _is_mrr_schema(view_name=view_name):
176-
return DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_MRR
177-
178-
if _is_revenue_item_schema(view_name=view_name):
179-
return DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_REVENUE_ITEM
180-
181-
return None
182-
183-
184-
def _is_customer_schema(view_name: str) -> bool:
185-
customer_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_CUSTOMER]
186-
return view_name.endswith(customer_schema.source_suffix) or view_name.endswith(customer_schema.events_suffix)
187-
188-
189-
def _is_mrr_schema(view_name: str) -> bool:
190-
mrr_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_MRR]
191-
return view_name.endswith(mrr_schema.source_suffix) or view_name.endswith(mrr_schema.events_suffix)
192-
193-
194-
def _is_revenue_item_schema(view_name: str) -> bool:
195-
revenue_item_schema = VIEW_SCHEMAS[DatabaseSchemaManagedViewTableKind.REVENUE_ANALYTICS_REVENUE_ITEM]
196-
return view_name.endswith(revenue_item_schema.source_suffix) or view_name.endswith(
197-
revenue_item_schema.events_suffix
198-
)
199-
200-
201-
def _is_event_view(view_name: str) -> bool:
202-
return _get_event_name(view_name) is not None
203-
204-
205-
def _get_event_name(view_name: str) -> str | None:
206-
return view_name.split(".")[2] if "revenue_analytics.events" in view_name else None
207-
208-
209160
class PersonsRevenueAnalyticsTable(LazyTable):
210161
fields: dict[str, FieldOrTable] = FIELDS
211162

0 commit comments

Comments
 (0)