From cea1b4fb7a9d17d1a40576e1e9b1837e658ddb18 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sat, 25 Mar 2023 13:28:16 +0100 Subject: [PATCH 01/22] really lazy tables attempt 1 --- posthog/hogql/ast.py | 16 +++- posthog/hogql/database.py | 95 ++++++++++++++----- posthog/hogql/printer.py | 4 +- posthog/hogql/test/test_resolver.py | 56 ++++++----- posthog/hogql/transforms/lazy_tables.py | 34 +++---- .../hogql/transforms/test/test_lazy_tables.py | 18 +++- posthog/hogql/visitor.py | 4 +- 7 files changed, 152 insertions(+), 75 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 453fba29c69b3..492b030e4ad0a 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -8,10 +8,11 @@ from posthog.hogql.database import ( DatabaseField, FieldTraverser, - LazyTable, + LazyJoin, StringJSONDatabaseField, Table, VirtualTable, + LazyTable, ) # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -76,6 +77,8 @@ def get_child(self, name: str) -> Ref: return AsteriskRef(table=self) if self.has_child(name): field = self.resolve_database_table().get_field(name) + if isinstance(field, LazyJoin): + return LazyJoinRef(table=self, field=name, lazy_join=field) if isinstance(field, LazyTable): return LazyTableRef(table=self, field=name, lazy_table=field) if isinstance(field, FieldTraverser): @@ -101,13 +104,22 @@ def resolve_database_table(self) -> Table: return self.table_ref.table +class LazyJoinRef(BaseTableRef): + table: BaseTableRef + field: str + lazy_join: LazyJoin + + def resolve_database_table(self) -> Table: + return self.lazy_join.join_table + + class LazyTableRef(BaseTableRef): table: BaseTableRef field: str lazy_table: LazyTable def resolve_database_table(self) -> Table: - return self.lazy_table.table + return self.lazy_table class VirtualTableRef(BaseTableRef): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 5333402e3a0ba..2aa7cc0409833 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -65,8 +65,7 @@ def get_asterisk(self) -> Dict[str, DatabaseField]: asterisk[key] = database_field elif ( isinstance(database_field, Table) - or isinstance(database_field, LazyTable) - or isinstance(database_field, VirtualTable) + or isinstance(database_field, LazyJoin) or isinstance(database_field, FieldTraverser) ): pass # ignore virtual tables for now @@ -75,15 +74,23 @@ def get_asterisk(self) -> Dict[str, DatabaseField]: return asterisk -class LazyTable(BaseModel): +class LazyJoin(BaseModel): class Config: extra = Extra.forbid join_function: Callable[[str, str, Dict[str, Any]], Any] - table: Table + join_table: Table from_field: str +class LazyTable(Table): + class Config: + extra = Extra.forbid + + def lazy_select(self, requested_fields: Dict[str, Any]) -> Any: + raise NotImplementedError("LazyTable.lazy_select not overridden") + + class VirtualTable(Table): class Config: extra = Extra.forbid @@ -124,7 +131,7 @@ def hogql_table(self): return "persons" -def join_with_persons_table(from_table: str, to_table: str, requested_fields: Dict[str, Any]): +def select_from_persons_table(requested_fields: Dict[str, Any]): from posthog.hogql import ast if not requested_fields: @@ -143,7 +150,6 @@ def join_with_persons_table(from_table: str, to_table: str, requested_fields: Di id = ast.Field(chain=["id"]) return ast.JoinExpr( - join_type="INNER JOIN", table=ast.SelectQuery( select=fields_to_select + [id], select_from=ast.JoinExpr(table=ast.Field(chain=["persons"])), @@ -153,14 +159,48 @@ def join_with_persons_table(from_table: str, to_table: str, requested_fields: Di left=argmax_version(ast.Field(chain=["is_deleted"])), right=ast.Constant(value=0), ), - ), - alias=to_table, - constraint=ast.CompareOperation( - op=ast.CompareOperationType.Eq, - left=ast.Field(chain=[from_table, "person_id"]), - right=ast.Field(chain=[to_table, "id"]), - ), + ) + ) + + +def join_with_persons_table(from_table: str, to_table: str, requested_fields: Dict[str, Any]): + from posthog.hogql import ast + + if not requested_fields: + raise ValueError("No fields requested from persons table. Why are we joining it?") + join_expr = select_from_persons_table(requested_fields) + join_expr.join_type = "INNER JOIN" + join_expr.alias = to_table + join_expr.constraint = ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=[from_table, "person_id"]), + right=ast.Field(chain=[to_table, "id"]), ) + return join_expr + + +class LazyPersonsTable(LazyTable): + id: StringDatabaseField = StringDatabaseField(name="id") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") + is_identified: BooleanDatabaseField = BooleanDatabaseField(name="is_identified") + + is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") + version: IntegerDatabaseField = IntegerDatabaseField(name="version") + + def lazy_select(self, requested_fields: Dict[str, Any]): + return select_from_persons_table(requested_fields) + + def avoid_asterisk_fields(self): + return ["is_deleted", "version"] + + # def clickhouse_table(self): + # raise + # # return "person" + + def hogql_table(self): + return "lazy_persons" class PersonDistinctIdTable(Table): @@ -170,7 +210,9 @@ class PersonDistinctIdTable(Table): is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") - person: LazyTable = LazyTable(from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table) + person: LazyJoin = LazyJoin( + from_field="person_id", join_table=PersonsTable(), join_function=join_with_persons_table + ) def avoid_asterisk_fields(self): return ["is_deleted", "version"] @@ -232,8 +274,10 @@ class EventsTable(Table): created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") # lazy table that adds a join to the persons table - pdi: LazyTable = LazyTable( - from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table + pdi: LazyJoin = LazyJoin( + from_field="distinct_id", + join_table=PersonDistinctIdTable(), + join_function=join_with_max_person_distinct_id_table, ) # person fields on the event itself poe: EventsPersonSubTable = EventsPersonSubTable() @@ -267,8 +311,10 @@ class SessionRecordingEvents(Table): last_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="last_event_timestamp") urls: StringDatabaseField = StringDatabaseField(name="urls", array=True) - pdi: LazyTable = LazyTable( - from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table + pdi: LazyJoin = LazyJoin( + from_field="distinct_id", + join_table=PersonDistinctIdTable(), + join_function=join_with_max_person_distinct_id_table, ) person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) @@ -290,7 +336,9 @@ class CohortPeople(Table): # TODO: automatically add "HAVING SUM(sign) > 0" to fields selected from this table? - person: LazyTable = LazyTable(from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table) + person: LazyJoin = LazyJoin( + from_field="person_id", join_table=PersonsTable(), join_function=join_with_persons_table + ) def clickhouse_table(self): return "cohortpeople" @@ -304,7 +352,9 @@ class StaticCohortPeople(Table): cohort_id: IntegerDatabaseField = IntegerDatabaseField(name="cohort_id") team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - person: LazyTable = LazyTable(from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table) + person: LazyJoin = LazyJoin( + from_field="person_id", join_table=PersonsTable(), join_function=join_with_persons_table + ) def avoid_asterisk_fields(self): return ["_timestamp", "_offset"] @@ -337,6 +387,7 @@ class Config: # Users can query from the tables below events: EventsTable = EventsTable() persons: PersonsTable = PersonsTable() + lazy_persons: LazyPersonsTable = LazyPersonsTable() person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() cohort_people: CohortPeople = CohortPeople() @@ -384,8 +435,8 @@ def serialize_database(database: Database) -> dict: fields.append({"key": field_key, "type": "boolean"}) elif isinstance(field, StringJSONDatabaseField): fields.append({"key": field_key, "type": "json"}) - elif isinstance(field, LazyTable): - fields.append({"key": field_key, "type": "lazy_table", "table": field.table.hogql_table()}) + elif isinstance(field, LazyJoin): + fields.append({"key": field_key, "type": "lazy_table", "table": field.join_table.hogql_table()}) elif isinstance(field, VirtualTable): fields.append( { diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index edd7a68e8cc3c..281faa62d7ed5 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -557,8 +557,8 @@ def visit_virtual_table_ref(self, ref: ast.VirtualTableRef): def visit_asterisk_ref(self, ref: ast.AsteriskRef): return "*" - def visit_lazy_table_ref(self, ref: ast.LazyTableRef): - raise ValueError("Unexpected ast.LazyTableRef. Make sure LazyTableResolver has run on the AST.") + def visit_lazy_join_ref(self, ref: ast.LazyJoinRef): + raise ValueError("Unexpected ast.LazyJoinRef. Make sure LazyJoinResolver has run on the AST.") def visit_field_traverser_ref(self, ref: ast.FieldTraverserRef): raise ValueError("Unexpected ast.FieldTraverserRef. This should have been resolved.") diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index b1dbe411df9eb..034aab2aafe30 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -252,8 +252,8 @@ def test_resolve_lazy_pdi_person_table(self): chain=["person", "id"], ref=ast.FieldRef( name="id", - table=ast.LazyTableRef( - table=pdi_table_ref, field="person", lazy_table=self.database.person_distinct_ids.person + table=ast.LazyJoinRef( + table=pdi_table_ref, field="person", lazy_join=self.database.person_distinct_ids.person ), ), ), @@ -269,9 +269,9 @@ def test_resolve_lazy_pdi_person_table(self): "distinct_id": ast.FieldRef(name="distinct_id", table=pdi_table_ref), "id": ast.FieldRef( name="id", - table=ast.LazyTableRef( + table=ast.LazyJoinRef( table=pdi_table_ref, - lazy_table=self.database.person_distinct_ids.person, + lazy_join=self.database.person_distinct_ids.person, field="person", ), ), @@ -299,9 +299,7 @@ def test_resolve_lazy_events_pdi_table(self): chain=["pdi", "person_id"], ref=ast.FieldRef( name="person_id", - table=ast.LazyTableRef( - table=events_table_ref, field="pdi", lazy_table=self.database.events.pdi - ), + table=ast.LazyJoinRef(table=events_table_ref, field="pdi", lazy_join=self.database.events.pdi), ), ), ], @@ -316,9 +314,9 @@ def test_resolve_lazy_events_pdi_table(self): "event": ast.FieldRef(name="event", table=events_table_ref), "person_id": ast.FieldRef( name="person_id", - table=ast.LazyTableRef( + table=ast.LazyJoinRef( table=events_table_ref, - lazy_table=self.database.events.pdi, + lazy_join=self.database.events.pdi, field="pdi", ), ), @@ -347,8 +345,8 @@ def test_resolve_lazy_events_pdi_table_aliased(self): chain=["e", "pdi", "person_id"], ref=ast.FieldRef( name="person_id", - table=ast.LazyTableRef( - table=events_table_alias_ref, field="pdi", lazy_table=self.database.events.pdi + table=ast.LazyJoinRef( + table=events_table_alias_ref, field="pdi", lazy_join=self.database.events.pdi ), ), ), @@ -365,9 +363,9 @@ def test_resolve_lazy_events_pdi_table_aliased(self): "event": ast.FieldRef(name="event", table=events_table_alias_ref), "person_id": ast.FieldRef( name="person_id", - table=ast.LazyTableRef( + table=ast.LazyJoinRef( table=events_table_alias_ref, - lazy_table=self.database.events.pdi, + lazy_join=self.database.events.pdi, field="pdi", ), ), @@ -395,12 +393,12 @@ def test_resolve_lazy_events_pdi_person_table(self): chain=["pdi", "person", "id"], ref=ast.FieldRef( name="id", - table=ast.LazyTableRef( - table=ast.LazyTableRef( - table=events_table_ref, field="pdi", lazy_table=self.database.events.pdi + table=ast.LazyJoinRef( + table=ast.LazyJoinRef( + table=events_table_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_table=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.table.person, ), ), ), @@ -416,12 +414,12 @@ def test_resolve_lazy_events_pdi_person_table(self): "event": ast.FieldRef(name="event", table=events_table_ref), "id": ast.FieldRef( name="id", - table=ast.LazyTableRef( - table=ast.LazyTableRef( - table=events_table_ref, field="pdi", lazy_table=self.database.events.pdi + table=ast.LazyJoinRef( + table=ast.LazyJoinRef( + table=events_table_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_table=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.table.person, ), ), }, @@ -449,12 +447,12 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): chain=["e", "pdi", "person", "id"], ref=ast.FieldRef( name="id", - table=ast.LazyTableRef( - table=ast.LazyTableRef( - table=events_table_alias_ref, field="pdi", lazy_table=self.database.events.pdi + table=ast.LazyJoinRef( + table=ast.LazyJoinRef( + table=events_table_alias_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_table=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.table.person, ), ), ), @@ -471,12 +469,12 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): "event": ast.FieldRef(name="event", table=events_table_alias_ref), "id": ast.FieldRef( name="id", - table=ast.LazyTableRef( - table=ast.LazyTableRef( - table=events_table_alias_ref, field="pdi", lazy_table=self.database.events.pdi + table=ast.LazyJoinRef( + table=ast.LazyJoinRef( + table=events_table_alias_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_table=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.table.person, ), ), }, diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 763a09bfaad50..974bf4b9efe13 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -2,9 +2,9 @@ from typing import Dict, List, Optional, cast from posthog.hogql import ast -from posthog.hogql.ast import LazyTableRef +from posthog.hogql.ast import LazyJoinRef from posthog.hogql.context import HogQLContext -from posthog.hogql.database import LazyTable +from posthog.hogql.database import LazyJoin from posthog.hogql.resolver import resolve_refs from posthog.hogql.visitor import TraversingVisitor @@ -12,19 +12,19 @@ def resolve_lazy_tables(node: ast.Expr, stack: Optional[List[ast.SelectQuery]] = None, context: HogQLContext = None): if stack: # TODO: remove this kludge for old props - LazyTableResolver(stack=stack, context=context).visit(stack[-1]) - LazyTableResolver(stack=stack, context=context).visit(node) + LazyJoinResolver(stack=stack, context=context).visit(stack[-1]) + LazyJoinResolver(stack=stack, context=context).visit(node) @dataclasses.dataclass class JoinToAdd: fields_accessed: Dict[str, ast.Expr] - lazy_table: LazyTable + lazy_join: LazyJoin from_table: str to_table: str -class LazyTableResolver(TraversingVisitor): +class LazyJoinResolver(TraversingVisitor): def __init__(self, stack: Optional[List[ast.SelectQuery]] = None, context: HogQLContext = None): super().__init__() self.stack_of_fields: List[List[ast.FieldRef | ast.PropertyRef]] = [[]] if stack else [] @@ -37,7 +37,7 @@ def _get_long_table_name(self, select: ast.SelectQueryRef, ref: ast.BaseTableRef return ref.name elif isinstance(ref, ast.SelectQueryAliasRef): return ref.name - elif isinstance(ref, ast.LazyTableRef): + elif isinstance(ref, ast.LazyJoinRef): return f"{self._get_long_table_name(select, ref.table)}__{ref.field}" elif isinstance(ref, ast.VirtualTableRef): return f"{self._get_long_table_name(select, ref.table)}__{ref.field}" @@ -48,7 +48,7 @@ def visit_property_ref(self, node: ast.PropertyRef): if node.joined_subquery is not None: # we have already visited this property return - if isinstance(node.parent.table, ast.LazyTableRef): + if isinstance(node.parent.table, ast.LazyJoinRef): if self.context and self.context.within_non_hogql_query: # If we're in a non-HogQL query, traverse deeper, just like we normally would have. self.visit(node.parent) @@ -59,7 +59,7 @@ def visit_property_ref(self, node: ast.PropertyRef): self.stack_of_fields[-1].append(node) def visit_field_ref(self, node: ast.FieldRef): - if isinstance(node.table, ast.LazyTableRef): + if isinstance(node.table, ast.LazyJoinRef): # Each time we find a field, we place it in a list for processing in "visit_select_query" if len(self.stack_of_fields) == 0: raise ValueError("Can't access a lazy field when not in a SelectQuery context") @@ -70,7 +70,7 @@ def visit_select_query(self, node: ast.SelectQuery): if not select_ref: raise ValueError("Select query must have a ref") - # Collect each `ast.Field` with `ast.LazyTableRef` + # Collect each `ast.Field` with `ast.LazyJoinRef` field_collector: List[ast.FieldRef] = [] self.stack_of_fields.append(field_collector) @@ -92,8 +92,8 @@ def visit_select_query(self, node: ast.SelectQuery): # Traverse the lazy tables until we reach a real table, collecting them in a list. # Usually there's just one or two. - table_refs: List[LazyTableRef] = [] - while isinstance(table_ref, ast.LazyTableRef): + table_refs: List[LazyJoinRef] = [] + while isinstance(table_ref, ast.LazyJoinRef): table_refs.append(table_ref) table_ref = table_ref.table @@ -104,14 +104,14 @@ def visit_select_query(self, node: ast.SelectQuery): if to_table not in joins_to_add: joins_to_add[to_table] = JoinToAdd( fields_accessed={}, # collect here all fields accessed on this table - lazy_table=table_ref.lazy_table, + lazy_join=table_ref.lazy_join, from_table=from_table, to_table=to_table, ) new_join = joins_to_add[to_table] if table_ref == field.table: chain = [] - if isinstance(table_ref, ast.LazyTableRef): + if isinstance(table_ref, ast.LazyJoinRef): chain.append(table_ref.resolve_database_table().hogql_table()) chain.append(field.name) if property is not None: @@ -125,8 +125,8 @@ def visit_select_query(self, node: ast.SelectQuery): # Without this "pdi.person.id" won't work if you did not ALSO select "pdi.person_id" explicitly for the join. for new_join in joins_to_add.values(): if new_join.from_table in joins_to_add: - joins_to_add[new_join.from_table].fields_accessed[new_join.lazy_table.from_field] = ast.Field( - chain=[new_join.lazy_table.from_field] + joins_to_add[new_join.from_table].fields_accessed[new_join.lazy_join.from_field] = ast.Field( + chain=[new_join.lazy_join.from_field] ) # Move the "last_join" pointer to the last join in the SELECT query @@ -136,7 +136,7 @@ def visit_select_query(self, node: ast.SelectQuery): # For all the collected joins, create the join subqueries, and add them to the table. for to_table, scope in joins_to_add.items(): - next_join = scope.lazy_table.join_function(scope.from_table, scope.to_table, scope.fields_accessed) + next_join = scope.lazy_join.join_function(scope.from_table, scope.to_table, scope.fields_accessed) resolve_refs(next_join, self.context.database, select_ref) select_ref.tables[to_table] = next_join.ref diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 0838718315560..cf5aad538c9e1 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -6,7 +6,7 @@ from posthog.test.base import BaseTest -class TestLazyTables(BaseTest): +class TestLazyJoins(BaseTest): def test_resolve_lazy_tables(self): printed = self._print_select("select event, pdi.person_id from events") expected = ( @@ -111,6 +111,22 @@ def test_resolve_lazy_tables_two_levels_properties_duplicate(self): ) self.assertEqual(printed, expected) + @override_settings(PERSON_ON_EVENTS_OVERRIDE=False) + def test_resolve_lazy_table_as_select_table(self): + printed = self._print_select("select id, properties.email, properties.$browser from lazy_persons") + expected = ( + "SELECT person_distinct_ids__person.`properties___$browser` " + "FROM person_distinct_id2 INNER JOIN " + "(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " + "AS `properties___$browser`, person.id FROM person " + f"WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " + "HAVING equals(argMax(person.is_deleted, person.version), 0)" + ") AS person_distinct_ids__person ON equals(person_distinct_id2.person_id, person_distinct_ids__person.id) " + f"WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " + "LIMIT 65535" + ) + self.assertEqual(printed, expected) + def _print_select(self, select: str): expr = parse_select(select) return print_ast(expr, HogQLContext(team_id=self.team.pk, enable_select_queries=True), "clickhouse") diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 382f38b4e9482..252e092ffeac0 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -118,10 +118,10 @@ def visit_select_union_query_ref(self, node: ast.SelectUnionQueryRef): def visit_table_ref(self, node: ast.TableRef): pass - def visit_field_traverser_ref(self, node: ast.LazyTableRef): + def visit_field_traverser_ref(self, node: ast.LazyJoinRef): self.visit(node.table) - def visit_lazy_table_ref(self, node: ast.LazyTableRef): + def visit_lazy_join_ref(self, node: ast.LazyJoinRef): self.visit(node.table) def visit_virtual_table_ref(self, node: ast.VirtualTableRef): From 23b0ae63fdab240bfc14e34678fd938fca510c7a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 29 Mar 2023 16:04:17 -0400 Subject: [PATCH 02/22] lazy tables --- posthog/hogql/ast.py | 8 +- posthog/hogql/database.py | 26 ++-- posthog/hogql/printer.py | 3 + posthog/hogql/resolver.py | 7 +- .../test/__snapshots__/test_database.ambr | 52 ++++++++ posthog/hogql/transforms/lazy_tables.py | 122 +++++++++++++----- .../hogql/transforms/test/test_lazy_tables.py | 13 +- posthog/hogql/visitor.py | 3 + 8 files changed, 170 insertions(+), 64 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 492b030e4ad0a..5a50b80d40fd6 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -80,7 +80,7 @@ def get_child(self, name: str) -> Ref: if isinstance(field, LazyJoin): return LazyJoinRef(table=self, field=name, lazy_join=field) if isinstance(field, LazyTable): - return LazyTableRef(table=self, field=name, lazy_table=field) + return LazyTableRef(table=field) if isinstance(field, FieldTraverser): return FieldTraverserRef(table=self, chain=field.chain) if isinstance(field, VirtualTable): @@ -114,12 +114,10 @@ def resolve_database_table(self) -> Table: class LazyTableRef(BaseTableRef): - table: BaseTableRef - field: str - lazy_table: LazyTable + table: LazyTable def resolve_database_table(self) -> Table: - return self.lazy_table + return self.table class VirtualTableRef(BaseTableRef): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 2aa7cc0409833..849fc8ad8a44d 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -135,7 +135,7 @@ def select_from_persons_table(requested_fields: Dict[str, Any]): from posthog.hogql import ast if not requested_fields: - raise ValueError("No fields requested from persons table. Why are we joining it?") + raise ValueError("No fields requested from persons table.") # contains the list of fields we will select from this table fields_to_select: List[ast.Expr] = [] @@ -149,17 +149,15 @@ def select_from_persons_table(requested_fields: Dict[str, Any]): id = ast.Field(chain=["id"]) - return ast.JoinExpr( - table=ast.SelectQuery( - select=fields_to_select + [id], - select_from=ast.JoinExpr(table=ast.Field(chain=["persons"])), - group_by=[id], - having=ast.CompareOperation( - op=ast.CompareOperationType.Eq, - left=argmax_version(ast.Field(chain=["is_deleted"])), - right=ast.Constant(value=0), - ), - ) + return ast.SelectQuery( + select=fields_to_select + [id], + select_from=ast.JoinExpr(table=ast.Field(chain=["persons"])), + group_by=[id], + having=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=argmax_version(ast.Field(chain=["is_deleted"])), + right=ast.Constant(value=0), + ), ) @@ -167,8 +165,8 @@ def join_with_persons_table(from_table: str, to_table: str, requested_fields: Di from posthog.hogql import ast if not requested_fields: - raise ValueError("No fields requested from persons table. Why are we joining it?") - join_expr = select_from_persons_table(requested_fields) + raise ValueError("No fields requested from persons table.") + join_expr = ast.JoinExpr(table=select_from_persons_table(requested_fields)) join_expr.join_type = "INNER JOIN" join_expr.alias = to_table join_expr.constraint = ast.CompareOperation( diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 281faa62d7ed5..8f89017133446 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -560,6 +560,9 @@ def visit_asterisk_ref(self, ref: ast.AsteriskRef): def visit_lazy_join_ref(self, ref: ast.LazyJoinRef): raise ValueError("Unexpected ast.LazyJoinRef. Make sure LazyJoinResolver has run on the AST.") + def visit_lazy_table_ref(self, ref: ast.LazyJoinRef): + raise ValueError("Unexpected ast.LazyTableRef. Make sure LazyJoinResolver has run on the AST.") + def visit_field_traverser_ref(self, ref: ast.FieldTraverserRef): raise ValueError("Unexpected ast.FieldTraverserRef. This should have been resolved.") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 5089307e9aa85..ea041b2169d16 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -91,7 +91,12 @@ def visit_join_expr(self, node): raise ResolverException(f'Already have joined a table called "{table_alias}". Can\'t redefine.') if self.database.has_table(table_name): - node.table.ref = ast.TableRef(table=self.database.get_table(table_name)) + database_table = self.database.get_table(table_name) + if isinstance(database_table, ast.LazyTable): + node.table.ref = ast.LazyTableRef(table=database_table) + else: + node.table.ref = ast.TableRef(table=database_table) + if table_alias == table_name: node.ref = node.table.ref else: diff --git a/posthog/hogql/test/__snapshots__/test_database.ambr b/posthog/hogql/test/__snapshots__/test_database.ambr index f5f15ea0e90e7..186ff80c0db02 100644 --- a/posthog/hogql/test/__snapshots__/test_database.ambr +++ b/posthog/hogql/test/__snapshots__/test_database.ambr @@ -88,6 +88,32 @@ "type": "integer" } ], + "lazy_persons": [ + { + "key": "id", + "type": "string" + }, + { + "key": "created_at", + "type": "datetime" + }, + { + "key": "properties", + "type": "json" + }, + { + "key": "is_identified", + "type": "boolean" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], "person_distinct_ids": [ { "key": "distinct_id", @@ -338,6 +364,32 @@ "type": "integer" } ], + "lazy_persons": [ + { + "key": "id", + "type": "string" + }, + { + "key": "created_at", + "type": "datetime" + }, + { + "key": "properties", + "type": "json" + }, + { + "key": "is_identified", + "type": "boolean" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], "person_distinct_ids": [ { "key": "distinct_id", diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 974bf4b9efe13..bc7c2ca621a61 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -1,10 +1,9 @@ import dataclasses -from typing import Dict, List, Optional, cast +from typing import Dict, List, Optional from posthog.hogql import ast -from posthog.hogql.ast import LazyJoinRef from posthog.hogql.context import HogQLContext -from posthog.hogql.database import LazyJoin +from posthog.hogql.database import LazyJoin, LazyTable from posthog.hogql.resolver import resolve_refs from posthog.hogql.visitor import TraversingVisitor @@ -12,8 +11,8 @@ def resolve_lazy_tables(node: ast.Expr, stack: Optional[List[ast.SelectQuery]] = None, context: HogQLContext = None): if stack: # TODO: remove this kludge for old props - LazyJoinResolver(stack=stack, context=context).visit(stack[-1]) - LazyJoinResolver(stack=stack, context=context).visit(node) + LazyTableResolver(stack=stack, context=context).visit(stack[-1]) + LazyTableResolver(stack=stack, context=context).visit(node) @dataclasses.dataclass @@ -24,7 +23,13 @@ class JoinToAdd: to_table: str -class LazyJoinResolver(TraversingVisitor): +@dataclasses.dataclass +class TableToAdd: + fields_accessed: Dict[str, ast.Expr] + lazy_table: LazyTable + + +class LazyTableResolver(TraversingVisitor): def __init__(self, stack: Optional[List[ast.SelectQuery]] = None, context: HogQLContext = None): super().__init__() self.stack_of_fields: List[List[ast.FieldRef | ast.PropertyRef]] = [[]] if stack else [] @@ -33,6 +38,8 @@ def __init__(self, stack: Optional[List[ast.SelectQuery]] = None, context: HogQL def _get_long_table_name(self, select: ast.SelectQueryRef, ref: ast.BaseTableRef) -> str: if isinstance(ref, ast.TableRef): return select.get_alias_for_table_ref(ref) + elif isinstance(ref, ast.LazyTableRef): + return ref.table.hogql_table() elif isinstance(ref, ast.TableAliasRef): return ref.name elif isinstance(ref, ast.SelectQueryAliasRef): @@ -48,7 +55,7 @@ def visit_property_ref(self, node: ast.PropertyRef): if node.joined_subquery is not None: # we have already visited this property return - if isinstance(node.parent.table, ast.LazyJoinRef): + if isinstance(node.parent.table, ast.LazyJoinRef) or isinstance(node.parent.table, ast.LazyTableRef): if self.context and self.context.within_non_hogql_query: # If we're in a non-HogQL query, traverse deeper, just like we normally would have. self.visit(node.parent) @@ -59,7 +66,7 @@ def visit_property_ref(self, node: ast.PropertyRef): self.stack_of_fields[-1].append(node) def visit_field_ref(self, node: ast.FieldRef): - if isinstance(node.table, ast.LazyJoinRef): + if isinstance(node.table, ast.LazyJoinRef) or isinstance(node.table, ast.LazyTableRef): # Each time we find a field, we place it in a list for processing in "visit_select_query" if len(self.stack_of_fields) == 0: raise ValueError("Can't access a lazy field when not in a SelectQuery context") @@ -79,6 +86,7 @@ def visit_select_query(self, node: ast.SelectQuery): # Collect all the joins we need to add to the select query joins_to_add: Dict[str, JoinToAdd] = {} + tables_to_add: Dict[str, TableToAdd] = {} for field_or_property in field_collector: if isinstance(field_or_property, ast.FieldRef): property = None @@ -92,34 +100,54 @@ def visit_select_query(self, node: ast.SelectQuery): # Traverse the lazy tables until we reach a real table, collecting them in a list. # Usually there's just one or two. - table_refs: List[LazyJoinRef] = [] - while isinstance(table_ref, ast.LazyJoinRef): + table_refs: List[ast.LazyJoinRef | ast.LazyTableRef] = [] + while isinstance(table_ref, ast.LazyJoinRef) or isinstance(table_ref, ast.LazyTableRef): table_refs.append(table_ref) table_ref = table_ref.table # Loop over the collected lazy tables in reverse order to create the joins for table_ref in reversed(table_refs): - from_table = self._get_long_table_name(select_ref, table_ref.table) - to_table = self._get_long_table_name(select_ref, table_ref) - if to_table not in joins_to_add: - joins_to_add[to_table] = JoinToAdd( - fields_accessed={}, # collect here all fields accessed on this table - lazy_join=table_ref.lazy_join, - from_table=from_table, - to_table=to_table, - ) - new_join = joins_to_add[to_table] - if table_ref == field.table: - chain = [] - if isinstance(table_ref, ast.LazyJoinRef): - chain.append(table_ref.resolve_database_table().hogql_table()) - chain.append(field.name) - if property is not None: - chain.append(property.name) - property.joined_subquery_field_name = f"{field.name}___{property.name}" - new_join.fields_accessed[property.joined_subquery_field_name] = ast.Field(chain=chain) - else: - new_join.fields_accessed[field.name] = ast.Field(chain=chain) + if isinstance(table_ref, ast.LazyJoinRef): + from_table = self._get_long_table_name(select_ref, table_ref.table) + to_table = self._get_long_table_name(select_ref, table_ref) + if to_table not in joins_to_add: + joins_to_add[to_table] = JoinToAdd( + fields_accessed={}, # collect here all fields accessed on this table + lazy_join=table_ref.lazy_join, + from_table=from_table, + to_table=to_table, + ) + new_join = joins_to_add[to_table] + if table_ref == field.table: + chain = [] + if isinstance(table_ref, ast.LazyJoinRef): + chain.append(table_ref.resolve_database_table().hogql_table()) + chain.append(field.name) + if property is not None: + chain.append(property.name) + property.joined_subquery_field_name = f"{field.name}___{property.name}" + new_join.fields_accessed[property.joined_subquery_field_name] = ast.Field(chain=chain) + else: + new_join.fields_accessed[field.name] = ast.Field(chain=chain) + elif isinstance(table_ref, ast.LazyTableRef): + table_name = self._get_long_table_name(select_ref, table_ref) + if table_name not in tables_to_add: + tables_to_add[table_name] = TableToAdd( + fields_accessed={}, # collect here all fields accessed on this table + lazy_table=table_ref.table, + ) + new_table = tables_to_add[table_name] + if table_ref == field.table: + chain = [] + # if isinstance(table_ref, ast.LazyTableRef) and table_ref.resolve_database_table(): + # chain.append(table_ref.resolve_database_table().hogql_table()) + chain.append(field.name) + if property is not None: + chain.append(property.name) + property.joined_subquery_field_name = f"{field.name}___{property.name}" + new_table.fields_accessed[property.joined_subquery_field_name] = ast.Field(chain=chain) + else: + new_table.fields_accessed[field.name] = ast.Field(chain=chain) # Make sure we also add fields we will use for the join's "ON" condition into the list of fields accessed. # Without this "pdi.person.id" won't work if you did not ALSO select "pdi.person_id" explicitly for the join. @@ -149,13 +177,37 @@ def visit_select_query(self, node: ast.SelectQuery): while last_join.next_join is not None: last_join = last_join.next_join + # For all the collected tables, create the subqueries, and add them to the table. + for table_name, table_to_add in tables_to_add.items(): + added_table = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed) + resolve_refs(added_table, self.context.database, select_ref) + old_table_ref = select_ref.tables[table_name] + select_ref.tables[table_name] = ast.SelectQueryAliasRef(name=table_name, ref=added_table.ref) + + join_ptr = node.select_from + while join_ptr: + if join_ptr.table.ref == old_table_ref: + join_ptr.table = added_table + join_ptr.ref = select_ref.tables[table_name] + join_ptr.alias = table_name + break + join_ptr = join_ptr.next_join + # Assign all refs on the fields we collected earlier for field_or_property in field_collector: if isinstance(field_or_property, ast.FieldRef): - to_table = self._get_long_table_name(select_ref, field_or_property.table) - field_or_property.table = select_ref.tables[to_table] + table_ref = field_or_property.table + elif isinstance(field_or_property, ast.PropertyRef): + table_ref = field_or_property.parent.table + else: + raise Exception("Should not be reachable") + + table_name = self._get_long_table_name(select_ref, table_ref) + table_ref = select_ref.tables[table_name] + + if isinstance(field_or_property, ast.FieldRef): + field_or_property.table = table_ref elif isinstance(field_or_property, ast.PropertyRef): - to_table = self._get_long_table_name(select_ref, cast(ast.PropertyRef, field_or_property).parent.table) - field_or_property.joined_subquery = select_ref.tables[to_table] + field_or_property.joined_subquery = table_ref self.stack_of_fields.pop() diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index cf5aad538c9e1..9876207902a6c 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -115,15 +115,10 @@ def test_resolve_lazy_tables_two_levels_properties_duplicate(self): def test_resolve_lazy_table_as_select_table(self): printed = self._print_select("select id, properties.email, properties.$browser from lazy_persons") expected = ( - "SELECT person_distinct_ids__person.`properties___$browser` " - "FROM person_distinct_id2 INNER JOIN " - "(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " - "AS `properties___$browser`, person.id FROM person " - f"WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - "HAVING equals(argMax(person.is_deleted, person.version), 0)" - ") AS person_distinct_ids__person ON equals(person_distinct_id2.person_id, person_distinct_ids__person.id) " - f"WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " - "LIMIT 65535" + f"SELECT lazy_persons.id, lazy_persons.properties___email, lazy_persons.`properties___$browser` FROM " + f"(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS " + f"properties___email, argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), person.version) " + f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS lazy_persons LIMIT 65535" ) self.assertEqual(printed, expected) diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 252e092ffeac0..e5f52d404560f 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -118,6 +118,9 @@ def visit_select_union_query_ref(self, node: ast.SelectUnionQueryRef): def visit_table_ref(self, node: ast.TableRef): pass + def visit_lazy_table_ref(self, node: ast.TableRef): + pass + def visit_field_traverser_ref(self, node: ast.LazyJoinRef): self.visit(node.table) From 090fa405ebde074acfd5f76fd94376a779788784 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 29 Mar 2023 17:02:09 -0400 Subject: [PATCH 03/22] can select if hogql --- posthog/hogql/printer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 8f89017133446..9be4ad3b48e66 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -260,6 +260,10 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: elif isinstance(node.ref, ast.SelectQueryAliasRef) and node.alias is not None: join_strings.append(self.visit(node.table)) join_strings.append(f"AS {self._print_identifier(node.alias)}") + + elif isinstance(node.ref, ast.LazyTableRef) and self.dialect == "hogql": + join_strings.append(self._print_identifier(node.ref.table.hogql_table())) + else: raise ValueError("Only selecting from a table or a subquery is supported") From 0c31345d39515f32a41576126c0cfd0196ae9a14 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 29 Mar 2023 17:11:27 -0400 Subject: [PATCH 04/22] fix small test bugs --- posthog/hogql/test/test_resolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 034aab2aafe30..d2e1463915405 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -398,7 +398,7 @@ def test_resolve_lazy_events_pdi_person_table(self): table=events_table_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_join=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.join_table.person, ), ), ), @@ -419,7 +419,7 @@ def test_resolve_lazy_events_pdi_person_table(self): table=events_table_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_join=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.join_table.person, ), ), }, @@ -452,7 +452,7 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): table=events_table_alias_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_join=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.join_table.person, ), ), ), @@ -474,7 +474,7 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): table=events_table_alias_ref, field="pdi", lazy_join=self.database.events.pdi ), field="person", - lazy_join=self.database.events.pdi.table.person, + lazy_join=self.database.events.pdi.join_table.person, ), ), }, From 81ffabd284abf903c897b7e201c6d82505b9f225 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 29 Mar 2023 17:44:15 -0400 Subject: [PATCH 05/22] more fixes --- posthog/hogql/ast.py | 2 + posthog/hogql/test/test_printer.py | 4 +- posthog/hogql/transforms/lazy_tables.py | 54 +++++++++++-------- .../hogql/transforms/test/test_lazy_tables.py | 18 +++++++ 4 files changed, 54 insertions(+), 24 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 5a50b80d40fd6..f257cb8ef230a 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -338,6 +338,8 @@ class Call(Expr): class JoinExpr(Expr): + ref: Optional[BaseTableRef | SelectQueryRef | SelectQueryAliasRef | SelectUnionQueryRef] + join_type: Optional[str] = None table: Optional[Union["SelectQuery", "SelectUnionQuery", Field]] = None alias: Optional[str] = None diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index ce17831c87c12..b89b670e14c0b 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -418,14 +418,14 @@ def test_select_sample(self): self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id" ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN person ON equals(person.id, events__pdi.person_id) INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN person ON equals(person.id, events__pdi.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", ) self.assertEqual( self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons SAMPLE 0.1 ON persons.id=events.person_id" ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN person SAMPLE 0.1 ON equals(person.id, events__pdi.person_id) INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN person SAMPLE 0.1 ON equals(person.id, events__pdi.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", ) with override_settings(PERSON_ON_EVENTS_OVERRIDE=True): diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index bc7c2ca621a61..16790c5f42ce0 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -157,37 +157,47 @@ def visit_select_query(self, node: ast.SelectQuery): chain=[new_join.lazy_join.from_field] ) - # Move the "last_join" pointer to the last join in the SELECT query - last_join = node.select_from - while last_join and last_join.next_join is not None: - last_join = last_join.next_join - # For all the collected joins, create the join subqueries, and add them to the table. - for to_table, scope in joins_to_add.items(): - next_join = scope.lazy_join.join_function(scope.from_table, scope.to_table, scope.fields_accessed) - resolve_refs(next_join, self.context.database, select_ref) - select_ref.tables[to_table] = next_join.ref - - # Link up the joins properly - if last_join is None: - node.select_from = next_join - last_join = next_join - else: - last_join.next_join = next_join - while last_join.next_join is not None: - last_join = last_join.next_join + for to_table, join_scope in joins_to_add.items(): + join_to_add: ast.JoinExpr = join_scope.lazy_join.join_function( + join_scope.from_table, join_scope.to_table, join_scope.fields_accessed + ) + resolve_refs(join_to_add, self.context.database, select_ref) + select_ref.tables[to_table] = join_to_add.ref + + join_ptr = node.select_from + added = False + while join_ptr: + if join_scope.from_table == join_ptr.alias or ( + isinstance(join_ptr.table, ast.Field) and join_scope.from_table == join_ptr.table.chain[0] + ): + join_to_add.next_join = join_ptr.next_join + join_ptr.next_join = join_to_add + added = True + break + if join_ptr.next_join: + join_ptr = join_ptr.next_join + else: + break + if not added: + if join_ptr: + join_ptr.next_join = join_to_add + elif node.select_from: + node.select_from.next_join = join_to_add + else: + node.select_from = join_to_add # For all the collected tables, create the subqueries, and add them to the table. for table_name, table_to_add in tables_to_add.items(): - added_table = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed) - resolve_refs(added_table, self.context.database, select_ref) + subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed) + resolve_refs(subquery, self.context.database, select_ref) old_table_ref = select_ref.tables[table_name] - select_ref.tables[table_name] = ast.SelectQueryAliasRef(name=table_name, ref=added_table.ref) + select_ref.tables[table_name] = ast.SelectQueryAliasRef(name=table_name, ref=subquery.ref) join_ptr = node.select_from while join_ptr: if join_ptr.table.ref == old_table_ref: - join_ptr.table = added_table + join_ptr.table = subquery join_ptr.ref = select_ref.tables[table_name] join_ptr.alias = table_name break diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 9876207902a6c..23be2cf6dc2fb 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -122,6 +122,24 @@ def test_resolve_lazy_table_as_select_table(self): ) self.assertEqual(printed, expected) + @override_settings(PERSON_ON_EVENTS_OVERRIDE=False) + def test_resolve_lazy_table_as_table_in_join(self): + printed = self._print_select( + "select event, distinct_id, events.person_id, lazy_persons.properties.email from events left join lazy_persons on lazy_persons.id = events.person_id limit 10" + ) + expected = ( + f"SELECT events.event, events.distinct_id, events__pdi.person_id, lazy_persons.properties___email FROM events " + f"INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, " + f"person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " + f"GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) " + f"AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) LEFT JOIN (SELECT " + f"argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS properties___email, " + f"person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS lazy_persons ON equals(lazy_persons.id, events__pdi.person_id) " + f"WHERE equals(events.team_id, {self.team.pk}) LIMIT 10" + ) + self.assertEqual(printed, expected) + def _print_select(self, select: str): expr = parse_select(select) return print_ast(expr, HogQLContext(team_id=self.team.pk, enable_select_queries=True), "clickhouse") From 20f0c106011d59ada78fdd63a1b64bd738c96aed Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Mar 2023 21:55:31 +0000 Subject: [PATCH 06/22] Update query snapshots --- posthog/queries/funnels/test/__snapshots__/test_funnel.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr index 34791264d2a5f..99973d9e267f2 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -286,8 +286,8 @@ WHERE team_id = 2 ) AS overrides ON e.person_id = overrides.old_person_id WHERE team_id = 2 AND event IN ['$autocapture', 'user signed up', '$autocapture', '$autocapture'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-03-17 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-03-24 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-03-22 00:00:00', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-03-29 23:59:59', 'UTC') AND notEmpty(e.person_id) AND (step_0 = 1 OR step_1 = 1 From 14ca551c186397813dd5515000af3b5577b2575d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 29 Mar 2023 23:59:40 -0400 Subject: [PATCH 07/22] freeze time for a funnel --- posthog/queries/funnels/test/test_funnel.py | 87 ++++++++++----------- 1 file changed, 43 insertions(+), 44 deletions(-) diff --git a/posthog/queries/funnels/test/test_funnel.py b/posthog/queries/funnels/test/test_funnel.py index cd3ede6efb0fe..4ca27c781f32a 100644 --- a/posthog/queries/funnels/test/test_funnel.py +++ b/posthog/queries/funnels/test/test_funnel.py @@ -176,53 +176,52 @@ def test_funnel_events(self): @override_settings(PERSON_ON_EVENTS_V2_OVERRIDE=True) @snapshot_clickhouse_queries def test_funnel_events_with_person_on_events_v2(self): + with freeze_time("2012-02-01T03:21:34.000Z"): + # KLUDGE: We need to do this to ensure create_person_id_override_by_distinct_id + # works correctly. Worth considering other approaches as we generally like to + # avoid truncating tables in tests for speed. + sync_execute("TRUNCATE TABLE sharded_events") + funnel = self._basic_funnel() + + # events + stopped_after_signup_person_id = uuid.uuid4() + person_factory(distinct_ids=["stopped_after_signup"], team_id=self.team.pk) + self._signup_event(distinct_id="stopped_after_signup", person_id=stopped_after_signup_person_id) + + stopped_after_pay_person_id = uuid.uuid4() + person_factory(distinct_ids=["stopped_after_pay"], team_id=self.team.pk) + self._signup_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) + self._pay_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) + + had_anonymous_id_person_id = uuid.uuid4() + person_factory(distinct_ids=["had_anonymous_id", "completed_movie"], team_id=self.team.pk) + self._signup_event(distinct_id="had_anonymous_id", person_id=had_anonymous_id_person_id) + self._pay_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) + self._movie_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) + + just_did_movie_person_id = uuid.uuid4() + person_factory(distinct_ids=["just_did_movie"], team_id=self.team.pk) + self._movie_event(distinct_id="just_did_movie", person_id=just_did_movie_person_id) + + wrong_order_person_id = uuid.uuid4() + person_factory(distinct_ids=["wrong_order"], team_id=self.team.pk) + self._pay_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + self._signup_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + self._movie_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + + create_person_id_override_by_distinct_id("stopped_after_signup", "stopped_after_pay", self.team.pk) - # KLUDGE: We need to do this to ensure create_person_id_override_by_distinct_id - # works correctly. Worth considering other approaches as we generally like to - # avoid truncating tables in tests for speed. - sync_execute("TRUNCATE TABLE sharded_events") - - funnel = self._basic_funnel() - - # events - stopped_after_signup_person_id = uuid.uuid4() - person_factory(distinct_ids=["stopped_after_signup"], team_id=self.team.pk) - self._signup_event(distinct_id="stopped_after_signup", person_id=stopped_after_signup_person_id) - - stopped_after_pay_person_id = uuid.uuid4() - person_factory(distinct_ids=["stopped_after_pay"], team_id=self.team.pk) - self._signup_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) - self._pay_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) - - had_anonymous_id_person_id = uuid.uuid4() - person_factory(distinct_ids=["had_anonymous_id", "completed_movie"], team_id=self.team.pk) - self._signup_event(distinct_id="had_anonymous_id", person_id=had_anonymous_id_person_id) - self._pay_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) - self._movie_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) - - just_did_movie_person_id = uuid.uuid4() - person_factory(distinct_ids=["just_did_movie"], team_id=self.team.pk) - self._movie_event(distinct_id="just_did_movie", person_id=just_did_movie_person_id) - - wrong_order_person_id = uuid.uuid4() - person_factory(distinct_ids=["wrong_order"], team_id=self.team.pk) - self._pay_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - self._signup_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - self._movie_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - - create_person_id_override_by_distinct_id("stopped_after_signup", "stopped_after_pay", self.team.pk) - - result = funnel.run() - self.assertEqual(result[0]["name"], "user signed up") + result = funnel.run() + self.assertEqual(result[0]["name"], "user signed up") - # key difference between this test and test_funnel_events. - # we merged two people and thus the count here is 3 and not 4 - self.assertEqual(result[0]["count"], 3) + # key difference between this test and test_funnel_events. + # we merged two people and thus the count here is 3 and not 4 + self.assertEqual(result[0]["count"], 3) - self.assertEqual(result[1]["name"], "paid") - self.assertEqual(result[1]["count"], 2) - self.assertEqual(result[2]["name"], "watched movie") - self.assertEqual(result[2]["count"], 1) + self.assertEqual(result[1]["name"], "paid") + self.assertEqual(result[1]["count"], 2) + self.assertEqual(result[2]["name"], "watched movie") + self.assertEqual(result[2]["count"], 1) def test_funnel_with_messed_up_order(self): action_play_movie = Action.objects.create(team=self.team, name="watched movie") From 43f3dae45de5385f4e22e6a7bb4fe282d1cd0e8e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 00:50:16 -0400 Subject: [PATCH 08/22] dogfood --- posthog/hogql/database.py | 169 ++++++++++-------- .../test/__snapshots__/test_database.ambr | 144 +++++++-------- posthog/hogql/test/test_query.py | 4 +- posthog/hogql/transforms/lazy_tables.py | 4 +- posthog/hogql/transforms/property_types.py | 4 +- .../hogql/transforms/test/test_lazy_tables.py | 12 +- .../transforms/test/test_property_types.py | 14 +- 7 files changed, 189 insertions(+), 162 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 849fc8ad8a44d..c58ff160193e4 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -103,43 +103,13 @@ class Config: chain: List[str] -class EventsPersonSubTable(VirtualTable): - id: StringDatabaseField = StringDatabaseField(name="person_id") - created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="person_created_at") - properties: StringJSONDatabaseField = StringJSONDatabaseField(name="person_properties") - - def clickhouse_table(self): - return "events" - - def hogql_table(self): - return "events" - - -class PersonsTable(Table): - id: StringDatabaseField = StringDatabaseField(name="id") - created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") - is_identified: BooleanDatabaseField = BooleanDatabaseField(name="is_identified") - is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") - version: IntegerDatabaseField = IntegerDatabaseField(name="version") - - def clickhouse_table(self): - return "person" - - def hogql_table(self): - return "persons" - - def select_from_persons_table(requested_fields: Dict[str, Any]): from posthog.hogql import ast if not requested_fields: raise ValueError("No fields requested from persons table.") - # contains the list of fields we will select from this table fields_to_select: List[ast.Expr] = [] - argmax_version: Callable[[ast.Expr], ast.Expr] = lambda field: ast.Call( name="argMax", args=[field, ast.Field(chain=["version"])] ) @@ -151,7 +121,7 @@ def select_from_persons_table(requested_fields: Dict[str, Any]): return ast.SelectQuery( select=fields_to_select + [id], - select_from=ast.JoinExpr(table=ast.Field(chain=["persons"])), + select_from=ast.JoinExpr(table=ast.Field(chain=["raw_persons"])), group_by=[id], having=ast.CompareOperation( op=ast.CompareOperationType.Eq, @@ -177,60 +147,46 @@ def join_with_persons_table(from_table: str, to_table: str, requested_fields: Di return join_expr -class LazyPersonsTable(LazyTable): +class RawPersonsTable(Table): id: StringDatabaseField = StringDatabaseField(name="id") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") is_identified: BooleanDatabaseField = BooleanDatabaseField(name="is_identified") - is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") - def lazy_select(self, requested_fields: Dict[str, Any]): - return select_from_persons_table(requested_fields) - - def avoid_asterisk_fields(self): - return ["is_deleted", "version"] - - # def clickhouse_table(self): - # raise - # # return "person" + def clickhouse_table(self): + return "person" def hogql_table(self): - return "lazy_persons" + return "raw_persons" -class PersonDistinctIdTable(Table): +class PersonsTable(LazyTable): + id: StringDatabaseField = StringDatabaseField(name="id") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") - person_id: StringDatabaseField = StringDatabaseField(name="person_id") - is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") - version: IntegerDatabaseField = IntegerDatabaseField(name="version") - - person: LazyJoin = LazyJoin( - from_field="person_id", join_table=PersonsTable(), join_function=join_with_persons_table - ) + properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") + is_identified: BooleanDatabaseField = BooleanDatabaseField(name="is_identified") - def avoid_asterisk_fields(self): - return ["is_deleted", "version"] + def lazy_select(self, requested_fields: Dict[str, Any]): + return select_from_persons_table(requested_fields) def clickhouse_table(self): - return "person_distinct_id2" + return "person" def hogql_table(self): - return "person_distinct_ids" + return "persons" -def join_with_max_person_distinct_id_table(from_table: str, to_table: str, requested_fields: Dict[str, Any]): +def select_from_person_distinct_ids_table(requested_fields: Dict[str, Any]): from posthog.hogql import ast if not requested_fields: requested_fields = {"person_id": ast.Field(chain=["person_id"])} - # contains the list of fields we will select from this table fields_to_select: List[ast.Expr] = [] - argmax_version: Callable[[ast.Expr], ast.Expr] = lambda field: ast.Call( name="argMax", args=[field, ast.Field(chain=["version"])] ) @@ -240,27 +196,78 @@ def join_with_max_person_distinct_id_table(from_table: str, to_table: str, reque distinct_id = ast.Field(chain=["distinct_id"]) - return ast.JoinExpr( - join_type="INNER JOIN", - table=ast.SelectQuery( - select=fields_to_select + [distinct_id], - select_from=ast.JoinExpr(table=ast.Field(chain=["person_distinct_ids"])), - group_by=[distinct_id], - having=ast.CompareOperation( - op=ast.CompareOperationType.Eq, - left=argmax_version(ast.Field(chain=["is_deleted"])), - right=ast.Constant(value=0), - ), - ), - alias=to_table, - constraint=ast.CompareOperation( + return ast.SelectQuery( + select=fields_to_select + [distinct_id], + select_from=ast.JoinExpr(table=ast.Field(chain=["raw_person_distinct_ids"])), + group_by=[distinct_id], + having=ast.CompareOperation( op=ast.CompareOperationType.Eq, - left=ast.Field(chain=[from_table, "distinct_id"]), - right=ast.Field(chain=[to_table, "distinct_id"]), + left=argmax_version(ast.Field(chain=["is_deleted"])), + right=ast.Constant(value=0), ), ) +def join_with_person_distinct_ids_table(from_table: str, to_table: str, requested_fields: Dict[str, Any]): + from posthog.hogql import ast + + if not requested_fields: + raise ValueError("No fields requested from person_distinct_ids.") + join_expr = ast.JoinExpr(table=select_from_person_distinct_ids_table(requested_fields)) + join_expr.join_type = "INNER JOIN" + join_expr.alias = to_table + join_expr.constraint = ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=[from_table, "distinct_id"]), + right=ast.Field(chain=[to_table, "distinct_id"]), + ) + return join_expr + + +class RawPersonDistinctIdTable(Table): + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") + person_id: StringDatabaseField = StringDatabaseField(name="person_id") + is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") + version: IntegerDatabaseField = IntegerDatabaseField(name="version") + + def clickhouse_table(self): + return "person_distinct_id2" + + def hogql_table(self): + return "raw_person_distinct_ids" + + +class PersonDistinctIdTable(LazyTable): + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") + person_id: StringDatabaseField = StringDatabaseField(name="person_id") + person: LazyJoin = LazyJoin( + from_field="person_id", join_table=PersonsTable(), join_function=join_with_persons_table + ) + + def lazy_select(self, requested_fields: Dict[str, Any]): + return select_from_person_distinct_ids_table(requested_fields) + + def clickhouse_table(self): + return "person_distinct_id2" + + def hogql_table(self): + return "person_distinct_ids" + + +class EventsPersonSubTable(VirtualTable): + id: StringDatabaseField = StringDatabaseField(name="person_id") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="person_created_at") + properties: StringJSONDatabaseField = StringJSONDatabaseField(name="person_properties") + + def clickhouse_table(self): + return "events" + + def hogql_table(self): + return "events" + + class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") event: StringDatabaseField = StringDatabaseField(name="event") @@ -275,7 +282,7 @@ class EventsTable(Table): pdi: LazyJoin = LazyJoin( from_field="distinct_id", join_table=PersonDistinctIdTable(), - join_function=join_with_max_person_distinct_id_table, + join_function=join_with_person_distinct_ids_table, ) # person fields on the event itself poe: EventsPersonSubTable = EventsPersonSubTable() @@ -312,7 +319,7 @@ class SessionRecordingEvents(Table): pdi: LazyJoin = LazyJoin( from_field="distinct_id", join_table=PersonDistinctIdTable(), - join_function=join_with_max_person_distinct_id_table, + join_function=join_with_person_distinct_ids_table, ) person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) @@ -384,13 +391,17 @@ class Config: # Users can query from the tables below events: EventsTable = EventsTable() + groups: Groups = Groups() persons: PersonsTable = PersonsTable() - lazy_persons: LazyPersonsTable = LazyPersonsTable() person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() + session_recording_events: SessionRecordingEvents = SessionRecordingEvents() cohort_people: CohortPeople = CohortPeople() static_cohort_people: StaticCohortPeople = StaticCohortPeople() - groups: Groups = Groups() + + raw_person_distinct_ids: RawPersonDistinctIdTable = RawPersonDistinctIdTable() + raw_persons: RawPersonsTable = RawPersonsTable() + # raw_cohort_people: RawCohortPeople = RawCohortPeople() def has_table(self, table_name: str) -> bool: return hasattr(self, table_name) diff --git a/posthog/hogql/test/__snapshots__/test_database.ambr b/posthog/hogql/test/__snapshots__/test_database.ambr index 186ff80c0db02..8d12e5bee43e7 100644 --- a/posthog/hogql/test/__snapshots__/test_database.ambr +++ b/posthog/hogql/test/__snapshots__/test_database.ambr @@ -62,9 +62,13 @@ ] } ], - "persons": [ + "groups": [ { - "key": "id", + "key": "index", + "type": "integer" + }, + { + "key": "key", "type": "string" }, { @@ -74,21 +78,9 @@ { "key": "properties", "type": "json" - }, - { - "key": "is_identified", - "type": "boolean" - }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" } ], - "lazy_persons": [ + "persons": [ { "key": "id", "type": "string" @@ -104,14 +96,6 @@ { "key": "is_identified", "type": "boolean" - }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" } ], "person_distinct_ids": [ @@ -123,14 +107,6 @@ "key": "person_id", "type": "string" }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" - }, { "key": "person", "type": "lazy_table", @@ -258,13 +234,27 @@ "table": "persons" } ], - "groups": [ + "raw_person_distinct_ids": [ { - "key": "index", - "type": "integer" + "key": "distinct_id", + "type": "string" }, { - "key": "key", + "key": "person_id", + "type": "string" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], + "raw_persons": [ + { + "key": "id", "type": "string" }, { @@ -274,6 +264,18 @@ { "key": "properties", "type": "json" + }, + { + "key": "is_identified", + "type": "boolean" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" } ] } @@ -338,9 +340,13 @@ "type": "string" } ], - "persons": [ + "groups": [ { - "key": "id", + "key": "index", + "type": "integer" + }, + { + "key": "key", "type": "string" }, { @@ -350,21 +356,9 @@ { "key": "properties", "type": "json" - }, - { - "key": "is_identified", - "type": "boolean" - }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" } ], - "lazy_persons": [ + "persons": [ { "key": "id", "type": "string" @@ -380,14 +374,6 @@ { "key": "is_identified", "type": "boolean" - }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" } ], "person_distinct_ids": [ @@ -399,14 +385,6 @@ "key": "person_id", "type": "string" }, - { - "key": "is_deleted", - "type": "boolean" - }, - { - "key": "version", - "type": "integer" - }, { "key": "person", "type": "lazy_table", @@ -534,13 +512,27 @@ "table": "persons" } ], - "groups": [ + "raw_person_distinct_ids": [ { - "key": "index", - "type": "integer" + "key": "distinct_id", + "type": "string" }, { - "key": "key", + "key": "person_id", + "type": "string" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" + } + ], + "raw_persons": [ + { + "key": "id", "type": "string" }, { @@ -550,6 +542,18 @@ { "key": "properties", "type": "json" + }, + { + "key": "is_identified", + "type": "boolean" + }, + { + "key": "is_deleted", + "type": "boolean" + }, + { + "key": "version", + "type": "integer" } ] } diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 133493d6c2a6a..8c0a22a284407 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -150,7 +150,7 @@ def test_query_joins_pdi(self): INNER JOIN ( SELECT distinct_id, argMax(person_id, version) as person_id - FROM person_distinct_ids + FROM raw_person_distinct_ids GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0 ) AS pdi @@ -169,7 +169,7 @@ def test_query_joins_pdi(self): ) self.assertEqual( response.hogql, - "SELECT event, timestamp, pdi.person_id FROM events AS e INNER JOIN (SELECT distinct_id, argMax(person_id, version) AS person_id FROM person_distinct_ids GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) LIMIT 100", + "SELECT event, timestamp, pdi.person_id FROM events AS e INNER JOIN (SELECT distinct_id, argMax(person_id, version) AS person_id FROM raw_person_distinct_ids GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) LIMIT 100", ) self.assertTrue(len(response.results) > 0) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 16790c5f42ce0..9e06f5642631c 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -120,8 +120,8 @@ def visit_select_query(self, node: ast.SelectQuery): new_join = joins_to_add[to_table] if table_ref == field.table: chain = [] - if isinstance(table_ref, ast.LazyJoinRef): - chain.append(table_ref.resolve_database_table().hogql_table()) + # if isinstance(table_ref, ast.LazyJoinRef): + # chain.append(table_ref.resolve_database_table().hogql_table()) chain.append(field.name) if property is not None: chain.append(property.name) diff --git a/posthog/hogql/transforms/property_types.py b/posthog/hogql/transforms/property_types.py index 9edcc9e3b3b2c..7952e6c3919ad 100644 --- a/posthog/hogql/transforms/property_types.py +++ b/posthog/hogql/transforms/property_types.py @@ -53,7 +53,7 @@ def visit_property_ref(self, node: ast.PropertyRef): if node.parent.name == "properties": if isinstance(node.parent.table, ast.BaseTableRef): table = node.parent.table.resolve_database_table().hogql_table() - if table == "persons": + if table == "persons" or table == "raw_persons": self.person_properties.add(node.name) if table == "events": self.event_properties.add(node.name) @@ -70,7 +70,7 @@ def visit_field(self, node: ast.Field): if isinstance(ref, ast.PropertyRef) and ref.parent.name == "properties": if isinstance(ref.parent.table, ast.BaseTableRef): table = ref.parent.table.resolve_database_table().hogql_table() - if table == "persons": + if table == "persons" or table == "raw_persons": if ref.name in self.person_properties: return self._add_type_to_string_field(node, self.person_properties[ref.name]) if table == "events": diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 23be2cf6dc2fb..31d6a0fe4dad1 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -113,29 +113,29 @@ def test_resolve_lazy_tables_two_levels_properties_duplicate(self): @override_settings(PERSON_ON_EVENTS_OVERRIDE=False) def test_resolve_lazy_table_as_select_table(self): - printed = self._print_select("select id, properties.email, properties.$browser from lazy_persons") + printed = self._print_select("select id, properties.email, properties.$browser from persons") expected = ( - f"SELECT lazy_persons.id, lazy_persons.properties___email, lazy_persons.`properties___$browser` FROM " + f"SELECT person.id, person.properties___email, person.`properties___$browser` FROM " f"(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS " f"properties___email, argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), person.version) " - f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS lazy_persons LIMIT 65535" + f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person LIMIT 65535" ) self.assertEqual(printed, expected) @override_settings(PERSON_ON_EVENTS_OVERRIDE=False) def test_resolve_lazy_table_as_table_in_join(self): printed = self._print_select( - "select event, distinct_id, events.person_id, lazy_persons.properties.email from events left join lazy_persons on lazy_persons.id = events.person_id limit 10" + "select event, distinct_id, events.person_id, persons.properties.email from events left join persons on persons.id = events.person_id limit 10" ) expected = ( - f"SELECT events.event, events.distinct_id, events__pdi.person_id, lazy_persons.properties___email FROM events " + f"SELECT events.event, events.distinct_id, events__pdi.person_id, person.properties___email FROM events " f"INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, " f"person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " f"GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) " f"AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) LEFT JOIN (SELECT " f"argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS properties___email, " f"person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS lazy_persons ON equals(lazy_persons.id, events__pdi.person_id) " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person ON equals(person.id, events__pdi.person_id) " f"WHERE equals(events.team_id, {self.team.pk}) LIMIT 10" ) self.assertEqual(printed, expected) diff --git a/posthog/hogql/transforms/test/test_property_types.py b/posthog/hogql/transforms/test/test_property_types.py index 0d1d27c0137f0..cedac8b71476f 100644 --- a/posthog/hogql/transforms/test/test_property_types.py +++ b/posthog/hogql/transforms/test/test_property_types.py @@ -54,9 +54,21 @@ def test_resolve_property_types_event(self): ) self.assertEqual(printed, expected) + def test_resolve_property_types_person_raw(self): + printed = self._print_select( + "select properties.tickets, properties.provided_timestamp, properties.$initial_browser from raw_persons" + ) + expected = ( + "SELECT toFloat64OrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '')), " + "parseDateTimeBestEffort(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " + "replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_2)s), '^\"|\"$', '') " + f"FROM person WHERE equals(person.team_id, {self.team.pk}) LIMIT 65535" + ) + self.assertEqual(printed, expected) + def test_resolve_property_types_person(self): printed = self._print_select( - "select properties.tickets, properties.provided_timestamp, properties.$initial_browser from persons" + "select properties.tickets, properties.provided_timestamp, properties.$initial_browser from raw_persons" ) expected = ( "SELECT toFloat64OrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '')), " From 3bd8645fb84dcbbf9cb902c3d48cd1b44120929d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 01:34:02 -0400 Subject: [PATCH 09/22] fix more tests --- posthog/hogql/printer.py | 10 ++++----- posthog/hogql/test/test_printer.py | 22 ++++++++++--------- posthog/hogql/test/test_query.py | 4 ++-- .../hogql/transforms/test/test_lazy_tables.py | 9 ++++---- 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 9be4ad3b48e66..12f87b0048769 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -242,11 +242,6 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: else: join_strings.append(self._print_identifier(node.ref.table.hogql_table())) - if node.sample is not None: - sample_clause = self.visit_sample_expr(node.sample) - if sample_clause is not None: - join_strings.append(sample_clause) - if self.dialect == "clickhouse": # TODO: do this in a separate pass before printing, along with person joins and other transforms extra_where = team_id_guard_for_table(node.ref, self.context) @@ -270,6 +265,11 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: if node.table_final: join_strings.append("FINAL") + if node.sample is not None: + sample_clause = self.visit_sample_expr(node.sample) + if sample_clause is not None: + join_strings.append(sample_clause) + if node.constraint is not None: join_strings.append(f"ON {self.visit(node.constraint)}") diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index b89b670e14c0b..e09d01063dd1e 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -418,29 +418,31 @@ def test_select_sample(self): self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id" ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN person ON equals(person.id, events__pdi.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN (SELECT person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons ON equals(persons.id, events__pdi.person_id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535", ) self.assertEqual( self._select( "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons SAMPLE 0.1 ON persons.id=events.person_id" ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN person SAMPLE 0.1 ON equals(person.id, events__pdi.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) JOIN (SELECT person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons SAMPLE 0.1 ON equals(persons.id, events__pdi.person_id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535", ) with override_settings(PERSON_ON_EVENTS_OVERRIDE=True): + expected = self._select( + "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id" + ) self.assertEqual( - self._select( - "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons ON persons.id=events.person_id" - ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN person ON equals(person.id, events.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + expected, + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN (SELECT person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons ON equals(persons.id, events.person_id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535", ) + expected = self._select( + "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons SAMPLE 0.1 ON persons.id=events.person_id" + ) self.assertEqual( - self._select( - "SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN persons SAMPLE 0.1 ON persons.id=events.person_id" - ), - f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN person SAMPLE 0.1 ON equals(person.id, events.person_id) WHERE and(equals(person.team_id, {self.team.pk}), equals(events.team_id, {self.team.pk})) LIMIT 65535", + expected, + f"SELECT events.event FROM events SAMPLE 2/78 OFFSET 999 JOIN (SELECT person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons SAMPLE 0.1 ON equals(persons.id, events.person_id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535", ) def test_count_distinct(self): diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 8c0a22a284407..c4bc5772c8d56 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -91,7 +91,7 @@ def test_query(self): ) self.assertEqual( response.clickhouse, - f"SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '') FROM person WHERE and(equals(person.team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), %(hogql_val_2)s)) LIMIT 100 SETTINGS readonly=1, max_execution_time=60", + f"SELECT DISTINCT persons.properties___sneaky_mail FROM (SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS properties___sneaky_mail, argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), person.version) AS properties___random_uuid, person.id FROM person WHERE equals(person.team_id, {self.team.id}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons WHERE equals(persons.properties___random_uuid, %(hogql_val_2)s) LIMIT 100 SETTINGS readonly=1, max_execution_time=60", ) self.assertEqual( response.hogql, @@ -105,7 +105,7 @@ def test_query(self): ) self.assertEqual( response.clickhouse, - f"SELECT DISTINCT person_distinct_id2.person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.id}) LIMIT 100 SETTINGS readonly=1, max_execution_time=60", + f"SELECT DISTINCT person_distinct_ids.person_id, person_distinct_ids.distinct_id FROM (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.id}) GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS person_distinct_ids LIMIT 100 SETTINGS readonly=1, max_execution_time=60", ) self.assertEqual( response.hogql, diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 31d6a0fe4dad1..2a2055b37fdf2 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -115,10 +115,11 @@ def test_resolve_lazy_tables_two_levels_properties_duplicate(self): def test_resolve_lazy_table_as_select_table(self): printed = self._print_select("select id, properties.email, properties.$browser from persons") expected = ( - f"SELECT person.id, person.properties___email, person.`properties___$browser` FROM " + f"SELECT persons.id, persons.properties___email, persons.`properties___$browser` FROM " f"(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS " f"properties___email, argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), person.version) " - f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person LIMIT 65535" + f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons LIMIT 65535" ) self.assertEqual(printed, expected) @@ -128,14 +129,14 @@ def test_resolve_lazy_table_as_table_in_join(self): "select event, distinct_id, events.person_id, persons.properties.email from events left join persons on persons.id = events.person_id limit 10" ) expected = ( - f"SELECT events.event, events.distinct_id, events__pdi.person_id, person.properties___email FROM events " + f"SELECT events.event, events.distinct_id, events__pdi.person_id, persons.properties___email FROM events " f"INNER JOIN (SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, " f"person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " f"GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) " f"AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) LEFT JOIN (SELECT " f"argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) AS properties___email, " f"person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person ON equals(person.id, events__pdi.person_id) " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS persons ON equals(persons.id, events__pdi.person_id) " f"WHERE equals(events.team_id, {self.team.pk}) LIMIT 10" ) self.assertEqual(printed, expected) From 5332d13d48cede921e305888ea3148f9a840fa2b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 02:03:44 -0400 Subject: [PATCH 10/22] move up --- posthog/hogql/transforms/lazy_tables.py | 33 ++++++++++--------- .../hogql/transforms/test/test_lazy_tables.py | 17 +++++----- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 9e06f5642631c..9b48c54c6aabe 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -157,6 +157,22 @@ def visit_select_query(self, node: ast.SelectQuery): chain=[new_join.lazy_join.from_field] ) + # For all the collected tables, create the subqueries, and add them to the table. + for table_name, table_to_add in tables_to_add.items(): + subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed) + resolve_refs(subquery, self.context.database, select_ref) + old_table_ref = select_ref.tables[table_name] + select_ref.tables[table_name] = ast.SelectQueryAliasRef(name=table_name, ref=subquery.ref) + + join_ptr = node.select_from + while join_ptr: + if join_ptr.table.ref == old_table_ref: + join_ptr.table = subquery + join_ptr.ref = select_ref.tables[table_name] + join_ptr.alias = table_name + break + join_ptr = join_ptr.next_join + # For all the collected joins, create the join subqueries, and add them to the table. for to_table, join_scope in joins_to_add.items(): join_to_add: ast.JoinExpr = join_scope.lazy_join.join_function( @@ -187,22 +203,6 @@ def visit_select_query(self, node: ast.SelectQuery): else: node.select_from = join_to_add - # For all the collected tables, create the subqueries, and add them to the table. - for table_name, table_to_add in tables_to_add.items(): - subquery = table_to_add.lazy_table.lazy_select(table_to_add.fields_accessed) - resolve_refs(subquery, self.context.database, select_ref) - old_table_ref = select_ref.tables[table_name] - select_ref.tables[table_name] = ast.SelectQueryAliasRef(name=table_name, ref=subquery.ref) - - join_ptr = node.select_from - while join_ptr: - if join_ptr.table.ref == old_table_ref: - join_ptr.table = subquery - join_ptr.ref = select_ref.tables[table_name] - join_ptr.alias = table_name - break - join_ptr = join_ptr.next_join - # Assign all refs on the fields we collected earlier for field_or_property in field_collector: if isinstance(field_or_property, ast.FieldRef): @@ -218,6 +218,7 @@ def visit_select_query(self, node: ast.SelectQuery): if isinstance(field_or_property, ast.FieldRef): field_or_property.table = table_ref elif isinstance(field_or_property, ast.PropertyRef): + field_or_property.parent.table = table_ref field_or_property.joined_subquery = table_ref self.stack_of_fields.pop() diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 2a2055b37fdf2..1a743cd7039bc 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -67,15 +67,14 @@ def test_resolve_lazy_tables_two_levels_traversed(self): def test_resolve_lazy_tables_one_level_properties(self): printed = self._print_select("select person.properties.$browser from person_distinct_ids") expected = ( - "SELECT person_distinct_ids__person.`properties___$browser` " - "FROM person_distinct_id2 INNER JOIN " - "(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " - "AS `properties___$browser`, person.id FROM person " - f"WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - "HAVING equals(argMax(person.is_deleted, person.version), 0)" - ") AS person_distinct_ids__person ON equals(person_distinct_id2.person_id, person_distinct_ids__person.id) " - f"WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " - "LIMIT 65535" + f"SELECT person_distinct_ids__person.`properties___$browser` FROM " + f"(SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id " + f"FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id " + f"HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS person_distinct_ids " + f"INNER JOIN (SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " + f"AS `properties___$browser`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person_distinct_ids__person " + f"ON equals(person_distinct_ids.person_id, person_distinct_ids__person.id) LIMIT 65535" ) self.assertEqual(printed, expected) From 7658fc1b9471c50bc489dddcda65a864ee54425f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 02:17:01 -0400 Subject: [PATCH 11/22] fix last test --- posthog/hogql/transforms/lazy_tables.py | 8 +++++++- posthog/hogql/transforms/test/test_lazy_tables.py | 9 ++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 9b48c54c6aabe..9389b1bda1920 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -87,7 +87,13 @@ def visit_select_query(self, node: ast.SelectQuery): # Collect all the joins we need to add to the select query joins_to_add: Dict[str, JoinToAdd] = {} tables_to_add: Dict[str, TableToAdd] = {} - for field_or_property in field_collector: + + # First properties, then fields. This way we always get the smallest units to query first. + sorted_properties = [property for property in field_collector if isinstance(property, ast.PropertyRef)] + [ + field for field in field_collector if not isinstance(field, ast.PropertyRef) + ] + + for field_or_property in sorted_properties: if isinstance(field_or_property, ast.FieldRef): property = None field = field_or_property diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index 1a743cd7039bc..2b7e167d944f1 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -102,11 +102,10 @@ def test_resolve_lazy_tables_two_levels_properties_duplicate(self): f"person_distinct_id2.distinct_id FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " f"GROUP BY person_distinct_id2.distinct_id HAVING equals(argMax(person_distinct_id2.is_deleted, " f"person_distinct_id2.version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) " - f"INNER JOIN (SELECT argMax(person.properties, person.version) AS properties, " - f"argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " - f"AS properties___name, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS events__pdi__person ON " - f"equals(events__pdi.person_id, events__pdi__person.id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535" + f"INNER JOIN (SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', ''), person.version) " + f"AS properties___name, argMax(person.properties, person.version) AS properties, person.id FROM person " + f"WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id HAVING equals(argMax(person.is_deleted, person.version), 0)) " + f"AS events__pdi__person ON equals(events__pdi.person_id, events__pdi__person.id) WHERE equals(events.team_id, {self.team.pk}) LIMIT 65535" ) self.assertEqual(printed, expected) From 96b35b90d0d6897b12f890ee72bb7465617241ce Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 17:17:51 -0400 Subject: [PATCH 12/22] few more fixes --- posthog/hogql/constants.py | 2 +- posthog/hogql/transforms/lazy_tables.py | 10 ++++++---- posthog/hogql/transforms/test/test_property_types.py | 4 ++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 9d1aecf083883..8eabde97f3d1c 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -13,7 +13,7 @@ "toFloat": "toFloat64OrNull", "toDecimal": "toDecimal64OrNull", "toDate": "toDateOrNull", - "toDateTime": "parseDateTimeBestEffort", + "toDateTime": "parseDateTimeBestEffortOrNull", "toIntervalSecond": "toIntervalSecond", "toIntervalMinute": "toIntervalMinute", "toIntervalHour": "toIntervalHour", diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 9389b1bda1920..3387b13f46848 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -89,9 +89,13 @@ def visit_select_query(self, node: ast.SelectQuery): tables_to_add: Dict[str, TableToAdd] = {} # First properties, then fields. This way we always get the smallest units to query first. - sorted_properties = [property for property in field_collector if isinstance(property, ast.PropertyRef)] + [ - field for field in field_collector if not isinstance(field, ast.PropertyRef) + matched_properties: List[ast.PropertyRef | ast.FieldRef] = [ + property for property in field_collector if isinstance(property, ast.PropertyRef) ] + matched_fields: List[ast.PropertyRef | ast.FieldRef] = [ + field for field in field_collector if isinstance(field, ast.FieldRef) + ] + sorted_properties: List[ast.PropertyRef | ast.FieldRef] = matched_properties + matched_fields for field_or_property in sorted_properties: if isinstance(field_or_property, ast.FieldRef): @@ -126,8 +130,6 @@ def visit_select_query(self, node: ast.SelectQuery): new_join = joins_to_add[to_table] if table_ref == field.table: chain = [] - # if isinstance(table_ref, ast.LazyJoinRef): - # chain.append(table_ref.resolve_database_table().hogql_table()) chain.append(field.name) if property is not None: chain.append(property.name) diff --git a/posthog/hogql/transforms/test/test_property_types.py b/posthog/hogql/transforms/test/test_property_types.py index cedac8b71476f..8deb2f0764ca0 100644 --- a/posthog/hogql/transforms/test/test_property_types.py +++ b/posthog/hogql/transforms/test/test_property_types.py @@ -60,7 +60,7 @@ def test_resolve_property_types_person_raw(self): ) expected = ( "SELECT toFloat64OrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '')), " - "parseDateTimeBestEffort(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " + "parseDateTimeBestEffortOrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " "replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_2)s), '^\"|\"$', '') " f"FROM person WHERE equals(person.team_id, {self.team.pk}) LIMIT 65535" ) @@ -72,7 +72,7 @@ def test_resolve_property_types_person(self): ) expected = ( "SELECT toFloat64OrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '')), " - "parseDateTimeBestEffort(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " + "parseDateTimeBestEffortOrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " "replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_2)s), '^\"|\"$', '') " f"FROM person WHERE equals(person.team_id, {self.team.pk}) LIMIT 65535" ) From 708dba28e3ccd5bdddeba75296a7b57243377ec1 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 00:50:16 -0400 Subject: [PATCH 13/22] dogfood From 627e4a7172109b203006fc3d7d916f198cabaadb Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 17:28:04 -0400 Subject: [PATCH 14/22] few more fixes --- posthog/hogql/database.py | 1 - posthog/hogql/transforms/lazy_tables.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index c58ff160193e4..4c92037a9dc4f 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -401,7 +401,6 @@ class Config: raw_person_distinct_ids: RawPersonDistinctIdTable = RawPersonDistinctIdTable() raw_persons: RawPersonsTable = RawPersonsTable() - # raw_cohort_people: RawCohortPeople = RawCohortPeople() def has_table(self, table_name: str) -> bool: return hasattr(self, table_name) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 3387b13f46848..8cb9fbaac8c3a 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -147,8 +147,6 @@ def visit_select_query(self, node: ast.SelectQuery): new_table = tables_to_add[table_name] if table_ref == field.table: chain = [] - # if isinstance(table_ref, ast.LazyTableRef) and table_ref.resolve_database_table(): - # chain.append(table_ref.resolve_database_table().hogql_table()) chain.append(field.name) if property is not None: chain.append(property.name) From d13c38353e67035c09baaa9487467f7e2feb962a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 30 Mar 2023 23:57:52 -0400 Subject: [PATCH 15/22] don't overreach --- posthog/queries/funnels/test/test_funnel.py | 85 ++++++++++----------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/posthog/queries/funnels/test/test_funnel.py b/posthog/queries/funnels/test/test_funnel.py index 4ca27c781f32a..c792a0c3921c2 100644 --- a/posthog/queries/funnels/test/test_funnel.py +++ b/posthog/queries/funnels/test/test_funnel.py @@ -176,52 +176,51 @@ def test_funnel_events(self): @override_settings(PERSON_ON_EVENTS_V2_OVERRIDE=True) @snapshot_clickhouse_queries def test_funnel_events_with_person_on_events_v2(self): - with freeze_time("2012-02-01T03:21:34.000Z"): - # KLUDGE: We need to do this to ensure create_person_id_override_by_distinct_id - # works correctly. Worth considering other approaches as we generally like to - # avoid truncating tables in tests for speed. - sync_execute("TRUNCATE TABLE sharded_events") - funnel = self._basic_funnel() - - # events - stopped_after_signup_person_id = uuid.uuid4() - person_factory(distinct_ids=["stopped_after_signup"], team_id=self.team.pk) - self._signup_event(distinct_id="stopped_after_signup", person_id=stopped_after_signup_person_id) - - stopped_after_pay_person_id = uuid.uuid4() - person_factory(distinct_ids=["stopped_after_pay"], team_id=self.team.pk) - self._signup_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) - self._pay_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) - - had_anonymous_id_person_id = uuid.uuid4() - person_factory(distinct_ids=["had_anonymous_id", "completed_movie"], team_id=self.team.pk) - self._signup_event(distinct_id="had_anonymous_id", person_id=had_anonymous_id_person_id) - self._pay_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) - self._movie_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) - - just_did_movie_person_id = uuid.uuid4() - person_factory(distinct_ids=["just_did_movie"], team_id=self.team.pk) - self._movie_event(distinct_id="just_did_movie", person_id=just_did_movie_person_id) - - wrong_order_person_id = uuid.uuid4() - person_factory(distinct_ids=["wrong_order"], team_id=self.team.pk) - self._pay_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - self._signup_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - self._movie_event(distinct_id="wrong_order", person_id=wrong_order_person_id) - - create_person_id_override_by_distinct_id("stopped_after_signup", "stopped_after_pay", self.team.pk) + # KLUDGE: We need to do this to ensure create_person_id_override_by_distinct_id + # works correctly. Worth considering other approaches as we generally like to + # avoid truncating tables in tests for speed. + sync_execute("TRUNCATE TABLE sharded_events") + funnel = self._basic_funnel() - result = funnel.run() - self.assertEqual(result[0]["name"], "user signed up") + # events + stopped_after_signup_person_id = uuid.uuid4() + person_factory(distinct_ids=["stopped_after_signup"], team_id=self.team.pk) + self._signup_event(distinct_id="stopped_after_signup", person_id=stopped_after_signup_person_id) + + stopped_after_pay_person_id = uuid.uuid4() + person_factory(distinct_ids=["stopped_after_pay"], team_id=self.team.pk) + self._signup_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) + self._pay_event(distinct_id="stopped_after_pay", person_id=stopped_after_pay_person_id) - # key difference between this test and test_funnel_events. - # we merged two people and thus the count here is 3 and not 4 - self.assertEqual(result[0]["count"], 3) + had_anonymous_id_person_id = uuid.uuid4() + person_factory(distinct_ids=["had_anonymous_id", "completed_movie"], team_id=self.team.pk) + self._signup_event(distinct_id="had_anonymous_id", person_id=had_anonymous_id_person_id) + self._pay_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) + self._movie_event(distinct_id="completed_movie", person_id=had_anonymous_id_person_id) - self.assertEqual(result[1]["name"], "paid") - self.assertEqual(result[1]["count"], 2) - self.assertEqual(result[2]["name"], "watched movie") - self.assertEqual(result[2]["count"], 1) + just_did_movie_person_id = uuid.uuid4() + person_factory(distinct_ids=["just_did_movie"], team_id=self.team.pk) + self._movie_event(distinct_id="just_did_movie", person_id=just_did_movie_person_id) + + wrong_order_person_id = uuid.uuid4() + person_factory(distinct_ids=["wrong_order"], team_id=self.team.pk) + self._pay_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + self._signup_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + self._movie_event(distinct_id="wrong_order", person_id=wrong_order_person_id) + + create_person_id_override_by_distinct_id("stopped_after_signup", "stopped_after_pay", self.team.pk) + + result = funnel.run() + self.assertEqual(result[0]["name"], "user signed up") + + # key difference between this test and test_funnel_events. + # we merged two people and thus the count here is 3 and not 4 + self.assertEqual(result[0]["count"], 3) + + self.assertEqual(result[1]["name"], "paid") + self.assertEqual(result[1]["count"], 2) + self.assertEqual(result[2]["name"], "watched movie") + self.assertEqual(result[2]["count"], 1) def test_funnel_with_messed_up_order(self): action_play_movie = Action.objects.create(team=self.team, name="watched movie") From db35d1b9282ea911b1a84ff8c643f1f4ad2b3d54 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 31 Mar 2023 04:06:03 +0000 Subject: [PATCH 16/22] Update query snapshots --- posthog/queries/funnels/test/__snapshots__/test_funnel.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr index c7a89e7ad3374..d183591dae223 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -286,8 +286,8 @@ WHERE team_id = 2 ) AS overrides ON e.person_id = overrides.old_person_id WHERE team_id = 2 AND event IN ['$autocapture', 'user signed up', '$autocapture', '$autocapture'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-03-23 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-03-30 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-03-24 00:00:00', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-03-31 23:59:59', 'UTC') AND notEmpty(e.person_id) AND (step_0 = 1 OR step_1 = 1 From 60bc4352d21dbc086e3a56247c1574d822234fde Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 10:35:21 +0000 Subject: [PATCH 17/22] Update query snapshots --- posthog/api/test/__snapshots__/test_feature_flag.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index edc478b652af3..99de0b31f4ff2 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -445,7 +445,7 @@ (SELECT feature_flag_key FROM existing_overrides) ) INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 116, + SELECT 120, person_id, key, 'random' @@ -526,7 +526,7 @@ (SELECT feature_flag_key FROM existing_overrides) ) INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 116, + SELECT 120, person_id, key, 'random' From 7283dec074260b936c711ab294d42bb51f852815 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 11:53:20 +0000 Subject: [PATCH 18/22] Update query snapshots --- posthog/api/test/__snapshots__/test_feature_flag.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index 99de0b31f4ff2..fc9357ccd0999 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -445,7 +445,7 @@ (SELECT feature_flag_key FROM existing_overrides) ) INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 120, + SELECT 119, person_id, key, 'random' @@ -526,7 +526,7 @@ (SELECT feature_flag_key FROM existing_overrides) ) INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 120, + SELECT 119, person_id, key, 'random' From 3568debd0b50ef3e809a4e9c6495ee4147721bda Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 11:57:28 +0000 Subject: [PATCH 19/22] Update UI snapshots for `chromium` (2) --- .../scenes-app-insights--trends-number.png | Bin 37805 -> 37780 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number.png b/frontend/__snapshots__/scenes-app-insights--trends-number.png index 814c35684a44136fcd2a1f95914439945f307aa3..5d44b4daea614497603d94eaefff6302f990c5fe 100644 GIT binary patch literal 37780 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%0DH0eIl=K zuTKjCeU~j-cp_!r6ay^jsnLxVJTeXZ3-_@I<6;Ge6?D5q>*xVH>fg61G^tQ{-5PSX(EPZQ1gZJ|L?)pZ@%FTyXmr2ntbA?chZc%p$uWK1BeK&we9o zW21TwlFgOf)SW4Xb(G{Nb%9}B+Rq?z%DwR!iBw#Bm(*C>7sWQu)Vf^K+(PaT6OUlZ zIrLW?3gIgQPf01(Y?<$OIfQt4=v@P;_cY--tV=|`Sm+;p;v30;ERB(a0rRj z=L)h~MbGw~%#Y`KaYO}EM4qMJz=3AlYB02o?4Y#t75D4#wH|`amyX>wf8F!G=aN-~ z2^QsJ0(b949Ku1US7#3VsD3H*aX57tW+zw`u^y({W)6}vGVfYRjiX(fl9=>w5oHH( zS3pDna1T_qtlq zh`dkNUn$jIvd2QdD%*Dfp4B&kNZ*Er+F&3%po#@RDx3oGnJEmnjTg>l#L^N_!oOcV zNVQ~nl;439jB{KZKuAnVpOn476{&AdM_$e%;q$@2Xvn5fm+w9;c?hssFt{)zA;E%X zQsvYK&yQ!8vR>NobF08z8r?y6@7msMvl+vxu>JH?;!Q8=S8xv#d_*3@tJ3YToSY}b z9e~{Xf1xj5M@LLT|0y&bXoP;YXY^H7(fq(4EpJjl;K7AjOSy-t{3 z+Q#@*E*2AzxkE@3STmuj6~ozj4tqFSE#FPvLPiG!&+(c6B7_+d<#t+CVEpQn@;4zUG_zHgKx64T5?F&N`{+5|jPl#E-Z9pGL(?v;Y z*ZqAsnyzc{(F>WK!t%iV3Y7lDzyHqy@c+_p4bzbmFL2Q@fD}hqo|F1i#5ap;qNhjv zpZ`LL#TP7DNu6aO$)6i0kJtN_^X9k>j~EiKN>BYvm{sHQ7jK*n^@g84|2`bV{y7}N zu0_*Wfoy>8vaPTIhoW~kE+ZJS$M1!zxj!tb4y3Gp+d99a@!jo+G&-iYuoQjO(WXhd8iSQe0u34)7 zrLpmVnpBKdd;d&lrykxqIBR?MFM(f&!{>GPQ6V9gSj!qp;~HG^u!FpFn?GiQKV`uy zF`0%OZS0?YrKL#qg6@C{znX8zav>XiFqEGl?J~}2xvLeO(r|mgTj?P$C#Sr~fApwC z5f_{o>a6<3pt-8F9T*}SRT)N#GnSmE?nvSK8-4J{#w}P6JCsA9g)25ZO9~4MPk%J- zHD$+2mq|pYU~zdlcR3=9?6f|YH@G>7TF8#Lrgm&Q~Z%BMe_8)<4&+iUqhn~K!W}@|Bcnp8@rpw z;f8_Z2GMdBS2sKsIqCihKzT&$eUy0)xIJU$7tT1|vtT2j zr?m(!SU=9rrV86&(t+9cu(wt|!ocAO&vvfZH-=b&Y^#o^BOojNe zfo110ZPB3c$3lZtK>)^=Esijg{P^)>z=!iKfmJFQJb$Ppw~YpR;#N8IC%9N^w)qG5 ztricQOHDq0lr^Z+Kx3h)>Ab@VP=9o&t90C;CV6a0c)Yv8^wY)*y2eV`JP(b!4;)1OBeCknI;egC!sr0eGA2_6F4z zEsB}>`PD^6439Diu*d<{%loJUgltMy4rE9Z2StJ_7ty~s?E$bIECM_2e}%I04N;bV zIefy!CD*~X7k3-=3#v8I&jq#{Rp3^L@7LUF7vC?To$_%z*#I4be@olzn|RU&wcULX z%I$|v+wEj0B`o-vYPS1hyNY~| z>>_-~Tqr~@en)q>pJ8Clg%D>om&L)o2?3x`BO;mz&{$-$JRv4hoWcrzQkZ)Um z74jJ5Am9uxEstf;xK?_AauOGD4|Z3dYHLO1#9hX}f~(6m+1A<$%Dxq7I|0R7JIEg* zKu*ZDAraY0jwT7cQx#;BAcW)MhrN6DByS9tRmYDLXb;akOz0fI6dCXj)2AUn0q0=& zk%7wP_6gZNSO6AMn&_1wZ9hk=fLo069|v|)ksG@(OoS{Ap+k@27F{_>yGz|n{)ZKQ z01H9(#F+kufFfg*AS=tu`wd`~Q*9NDLiE0_I8V=@{dw1QOv-ignVW7m;B-+%)}VLV zyYEv{QuYV%a6+(5-MWFaTw&=)IC2nZlX0ihsofs62h&2}j~HUELYa7VEiNtX4(R{~ zb((|kTf_uNBR}=@5Vy(IJ~6$U>iIwwYx<~R+_Ts7C|M@R$<_j!BSSh`kOOWD21RTy z2jg3;WH?ZToMohJx>0c=Rk?dN$02f!qVw0u23#tQ56lL*S;51*N>vP%gH$mdJ8X~G zsoi=o07rHuxmyn#C*ZAe^p*anrTp5Vbc8QF$O=4F#C1F%n!15lgrev*)&~X!9&ei> z9ydNj|4)h^4L*mE8rSbc+)Xj~MqG;LS5B2$M6mxn@ zmBo26>Rv|ilaW_T%0QD9NH9!J5sPs;u;LCI+~6~mzT5LQ2?I}hfK<%f++4OlkdeCc z_p20XwpHt-E6Lw9BX!6NDW)C8sAKS_MlN3;Qx^QXE||o zBvBv!-+b0Mjo<#aeYK32aN~$GAaxCOeE0Z0oM5U!NfP{XcCKjKcr` From 7d19306b5b0fbbca129d5ce6d47a28c2d2b2605e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:14:59 +0000 Subject: [PATCH 20/22] Update UI snapshots for `chromium` (2) --- .../scenes-app-insights--trends-number.png | Bin 37780 -> 37805 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-insights--trends-number.png b/frontend/__snapshots__/scenes-app-insights--trends-number.png index 5d44b4daea614497603d94eaefff6302f990c5fe..814c35684a44136fcd2a1f95914439945f307aa3 100644 GIT binary patch literal 37805 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%0DH0eIl=K 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 literal 37780 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 o2r0mS Date: Thu, 6 Apr 2023 15:07:38 +0200 Subject: [PATCH 21/22] fix merge issues --- posthog/hogql/transforms/lazy_tables.py | 4 ++-- .../hogql/transforms/test/test_lazy_tables.py | 17 ++++++++--------- .../transforms/test/test_property_types.py | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 10f5eb7002766..7855c5c5c2668 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -133,7 +133,7 @@ def visit_select_query(self, node: ast.SelectQuery): chain.append(field.name) if property is not None: chain.extend(property.chain) - property.joined_subquery_field_name = f"{field.name}___{property.name}" + property.joined_subquery_field_name = f"{field.name}___{'___'.join(property.chain)}" new_join.fields_accessed[property.joined_subquery_field_name] = ast.Field(chain=chain) else: new_join.fields_accessed[field.name] = ast.Field(chain=chain) @@ -150,7 +150,7 @@ def visit_select_query(self, node: ast.SelectQuery): chain.append(field.name) if property is not None: chain.extend(property.chain) - property.joined_subquery_field_name = f"{field.name}___{property.name}" + property.joined_subquery_field_name = f"{field.name}___{'___'.join(property.chain)}" new_table.fields_accessed[property.joined_subquery_field_name] = ast.Field(chain=chain) else: new_table.fields_accessed[field.name] = ast.Field(chain=chain) diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py index b739487e85760..9ad5282a666d7 100644 --- a/posthog/hogql/transforms/test/test_lazy_tables.py +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -82,15 +82,14 @@ def test_resolve_lazy_tables_one_level_properties(self): def test_resolve_lazy_tables_one_level_properties_deep(self): printed = self._print_select("select person.properties.$browser.in.json from person_distinct_ids") expected = ( - "SELECT person_distinct_ids__person.`properties___$browser___in___json` " - "FROM person_distinct_id2 INNER JOIN " - "(SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s, %(hogql_val_1)s, %(hogql_val_2)s), '^\"|\"$', ''), person.version) " - "AS `properties___$browser___in___json`, person.id FROM person " - f"WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " - "HAVING equals(argMax(person.is_deleted, person.version), 0)" - ") AS person_distinct_ids__person ON equals(person_distinct_id2.person_id, person_distinct_ids__person.id) " - f"WHERE equals(person_distinct_id2.team_id, {self.team.pk}) " - "LIMIT 65535" + f"SELECT person_distinct_ids__person.`properties___$browser___in___json` FROM " + f"(SELECT argMax(person_distinct_id2.person_id, person_distinct_id2.version) AS person_id, person_distinct_id2.distinct_id " + f"FROM person_distinct_id2 WHERE equals(person_distinct_id2.team_id, {self.team.pk}) GROUP BY person_distinct_id2.distinct_id " + f"HAVING equals(argMax(person_distinct_id2.is_deleted, person_distinct_id2.version), 0)) AS person_distinct_ids " + f"INNER JOIN (SELECT argMax(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s, %(hogql_val_1)s, %(hogql_val_2)s), '^\"|\"$', ''), person.version) " + f"AS `properties___$browser___in___json`, person.id FROM person WHERE equals(person.team_id, {self.team.pk}) GROUP BY person.id " + f"HAVING equals(argMax(person.is_deleted, person.version), 0)) AS person_distinct_ids__person " + f"ON equals(person_distinct_ids.person_id, person_distinct_ids__person.id) LIMIT 65535" ) self.assertEqual(printed, expected) diff --git a/posthog/hogql/transforms/test/test_property_types.py b/posthog/hogql/transforms/test/test_property_types.py index 6f06fd75f03a8..eca0a3020e2a4 100644 --- a/posthog/hogql/transforms/test/test_property_types.py +++ b/posthog/hogql/transforms/test/test_property_types.py @@ -60,8 +60,8 @@ def test_resolve_property_types_person_raw(self): ) expected = ( "SELECT toFloat64OrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_0)s), '^\"|\"$', '')), " - "parseDateTimeBestEffortOrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', '')), " - "replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_2)s), '^\"|\"$', '') " + "toDateTimeOrNull(replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_1)s), '^\"|\"$', ''), %(hogql_val_2)s), " + "replaceRegexpAll(JSONExtractRaw(person.properties, %(hogql_val_3)s), '^\"|\"$', '') " f"FROM person WHERE equals(person.team_id, {self.team.pk}) LIMIT 65535" ) self.assertEqual(printed, expected) From f058242e73184e082bc080b6b3eda3d44734b3ef Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 13:17:08 +0000 Subject: [PATCH 22/22] Update query snapshots --- .../test/__snapshots__/test_feature_flag.ambr | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index fc9357ccd0999..1ce586bf89e2b 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -419,7 +419,8 @@ # name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db ' WITH target_person_ids AS - (SELECT person_id + (SELECT team_id, + person_id FROM posthog_persondistinctid WHERE team_id = 2 AND distinct_id IN ('example_id', @@ -445,7 +446,7 @@ (SELECT feature_flag_key FROM existing_overrides) ) INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 119, + SELECT team_id, person_id, key, 'random' @@ -499,44 +500,7 @@ ' SELECT pg_sleep(1); - WITH target_person_ids AS - (SELECT person_id - FROM posthog_persondistinctid - WHERE team_id = 2 - AND distinct_id IN ('example_id', - 'random') ), - existing_overrides AS - (SELECT team_id, - person_id, - feature_flag_key, - hash_key - FROM posthog_featureflaghashkeyoverride - WHERE team_id = 2 - AND person_id IN - (SELECT person_id - FROM target_person_ids) ), - flags_to_override AS - (SELECT key - FROM posthog_featureflag - WHERE team_id = 2 - AND ensure_experience_continuity = TRUE - AND active = TRUE - AND deleted = FALSE - AND key NOT IN - (SELECT feature_flag_key - FROM existing_overrides) ) - INSERT INTO posthog_featureflaghashkeyoverride (team_id, person_id, feature_flag_key, hash_key) - SELECT 119, - person_id, - key, - 'random' - FROM flags_to_override, - target_person_ids - WHERE EXISTS - (SELECT 1 - FROM posthog_person - WHERE id = person_id - AND team_id = 2) ON CONFLICT DO NOTHING + SAVEPOINT _snapshot_ ' --- # name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db.5