Skip to content

Commit

Permalink
feat(hogql): hidden aliases for special fields
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra committed Nov 17, 2023
1 parent 4557575 commit 9bcb5c3
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 5 deletions.
4 changes: 4 additions & 0 deletions posthog/hogql/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,10 @@ class LambdaArgumentType(Type):
class Alias(Expr):
alias: str
expr: Expr
# Hidden aliases are created when HogQL invisibly renames fields, e.g "events.timestamp" gets turned into a
# "toDateTime(...) as timestamp". Unlike normal aliases, hidden aliases can be overriden. Visible aliases will throw
# if overridden, hidden ones will noop. Hidden aliases are shown if used as select columns in a subquery.
hidden: bool = False


class ArithmeticOperationOp(str, Enum):
Expand Down
9 changes: 7 additions & 2 deletions posthog/hogql/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,8 +810,13 @@ def visit_placeholder(self, node: ast.Placeholder):
raise HogQLException(f"Placeholders, such as {{{node.field}}}, are not supported in this context")

def visit_alias(self, node: ast.Alias):
inside = self.visit(node.expr)
if isinstance(node.expr, ast.Alias):
if node.hidden:
return self.visit(node.expr)
expr = node.expr
while isinstance(expr, ast.Alias) and expr.hidden:
expr = expr.expr
inside = self.visit(expr)
if isinstance(expr, ast.Alias):
inside = f"({inside})"
alias = self._print_identifier(node.alias)
return f"{inside} AS {alias}"
Expand Down
37 changes: 34 additions & 3 deletions posthog/hogql/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ def visit_select_query(self, node: ast.SelectQuery):
new_node.array_join_list = [self.visit(expr) for expr in node.array_join_list]

# Visit all the "SELECT a,b,c" columns. Mark each for export in "columns".
hidden_aliases = {}
for expr in node.select or []:
new_expr = self.visit(expr)

Expand All @@ -128,15 +129,30 @@ def visit_select_query(self, node: ast.SelectQuery):

# not an asterisk
if isinstance(new_expr.type, ast.FieldAliasType):
node_type.columns[new_expr.type.alias] = new_expr.type
alias = new_expr.type.alias
elif isinstance(new_expr.type, ast.FieldType):
node_type.columns[new_expr.type.name] = new_expr.type
alias = new_expr.type.name
elif isinstance(new_expr, ast.Alias):
node_type.columns[new_expr.alias] = new_expr.type
alias = new_expr.alias

if alias:
if isinstance(new_expr, ast.Alias) and new_expr.hidden:
hidden_aliases[alias] = new_expr
else:
node_type.columns[alias] = new_expr.type

# add the column to the new select query
new_node.select.append(new_expr)

# this dict will contain the last used hidden alias with this name
for key, hidden_alias in hidden_aliases.items():
# if no column took this alias, unhide it
if key not in node_type.columns:
node_type.columns[key] = hidden_alias.type
hidden_alias.hidden = False
if isinstance(hidden_alias.type, ast.FieldAliasType):
hidden_alias.type.hidden = False

# :TRICKY: Make sure to clone and visit _all_ SelectQuery nodes.
new_node.where = self.visit(node.where)
new_node.prewhere = self.visit(node.prewhere)
Expand Down Expand Up @@ -457,6 +473,21 @@ def visit_field(self, node: ast.Field):
message=f"Field '{node.type.name}' is of type '{node.type.resolve_constant_type().print_type()}'",
)

if isinstance(node.type, ast.FieldType):
return ast.Alias(
alias=node.type.name,
expr=node,
hidden=True,
type=ast.FieldAliasType(alias=node.type.name, type=node.type),
)
elif isinstance(node.type, ast.PropertyType):
return ast.Alias(
alias=node.type.chain[-1],
expr=node,
hidden=True,
type=ast.FieldAliasType(alias=node.type.chain[-1], type=node.type),
)

return node

def visit_array_access(self, node: ast.ArrayAccess):
Expand Down
61 changes: 61 additions & 0 deletions posthog/hogql/test/test_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1280,3 +1280,64 @@ def test_large_pretty_print(self):
"""
)
assert printed == self.snapshot

def test_print_hidden_aliases_timestamp(self):
query = parse_select("select * from (SELECT timestamp, timestamp FROM events)")
printed = print_ast(
query,
HogQLContext(team_id=self.team.pk, enable_select_queries=True),
dialect="clickhouse",
settings=HogQLGlobalSettings(max_execution_time=10),
)
self.assertEqual(
printed,
f"SELECT timestamp FROM (SELECT toTimeZone(events.timestamp, %(hogql_val_0)s), "
f"toTimeZone(events.timestamp, %(hogql_val_1)s) AS timestamp FROM events WHERE equals(events.team_id, {self.team.pk})) "
f"LIMIT 10000 SETTINGS readonly=2, max_execution_time=10, allow_experimental_object_type=1",
)

def test_print_hidden_aliases_column_override(self):
query = parse_select("select * from (SELECT timestamp as event, event FROM events)")
printed = print_ast(
query,
HogQLContext(team_id=self.team.pk, enable_select_queries=True),
dialect="clickhouse",
settings=HogQLGlobalSettings(max_execution_time=10),
)
self.assertEqual(
printed,
f"SELECT event FROM (SELECT toTimeZone(events.timestamp, %(hogql_val_0)s) AS event, "
f"event FROM events WHERE equals(events.team_id, {self.team.pk})) "
f"LIMIT 10000 SETTINGS readonly=2, max_execution_time=10, allow_experimental_object_type=1",
)

def test_print_hidden_aliases_properties(self):
query = parse_select("select * from (SELECT properties.$browser FROM events)")
printed = print_ast(
query,
HogQLContext(team_id=self.team.pk, enable_select_queries=True),
dialect="clickhouse",
settings=HogQLGlobalSettings(max_execution_time=10),
)
self.assertEqual(
printed,
f"SELECT `$browser` FROM (SELECT nullIf(nullIf(events.`mat_$browser`, ''), 'null') AS `$browser` "
f"FROM events WHERE equals(events.team_id, {self.team.pk})) LIMIT 10000 "
f"SETTINGS readonly=2, max_execution_time=10, allow_experimental_object_type=1",
)

def test_print_hidden_aliases_double_property(self):
query = parse_select("select * from (SELECT properties.$browser, properties.$browser FROM events)")
printed = print_ast(
query,
HogQLContext(team_id=self.team.pk, enable_select_queries=True),
dialect="clickhouse",
settings=HogQLGlobalSettings(max_execution_time=10),
)
self.assertEqual(
printed,
f"SELECT `$browser` FROM (SELECT nullIf(nullIf(events.`mat_$browser`, ''), 'null'), "
f"nullIf(nullIf(events.`mat_$browser`, ''), 'null') AS `$browser` " # only the second one gets the alias
f"FROM events WHERE equals(events.team_id, {self.team.pk})) LIMIT 10000 "
f"SETTINGS readonly=2, max_execution_time=10, allow_experimental_object_type=1",
)
1 change: 1 addition & 0 deletions posthog/hogql/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ def visit_alias(self, node: ast.Alias):
end=None if self.clear_locations else node.end,
type=None if self.clear_types else node.type,
alias=node.alias,
hidden=node.hidden,
expr=self.visit(node.expr),
)

Expand Down

0 comments on commit 9bcb5c3

Please sign in to comment.