From 7e4419531caa5b4d5c198e14929d4d697b856382 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 11:42:45 +0100 Subject: [PATCH 001/142] feat(hogql): select statements --- frontend/src/queries/schema.json | 57 +++--- frontend/src/queries/schema.ts | 14 +- posthog/hogql/ast.py | 28 ++- posthog/hogql/constants.py | 3 + posthog/hogql/context.py | 5 + posthog/hogql/parser.py | 185 +++++++++++++++--- posthog/hogql/printer.py | 73 ++++++- posthog/hogql/test/test_parser.py | 238 ++++++++++++++++++++++- posthog/hogql/test/test_printer.py | 97 +++++++++ posthog/models/event/query_event_list.py | 10 +- posthog/schema.py | 6 +- 11 files changed, 641 insertions(+), 75 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 1ac0c8e20d62e..58353872860da 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1600,34 +1600,8 @@ "type": "array" }, "response": { - "additionalProperties": false, - "description": "Cached query response", - "properties": { - "columns": { - "items": { - "type": "string" - }, - "type": "array" - }, - "hasMore": { - "type": "boolean" - }, - "results": { - "items": { - "items": {}, - "type": "array" - }, - "type": "array" - }, - "types": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": ["columns", "types", "results"], - "type": "object" + "$ref": "#/definitions/EventsQueryResponse", + "description": "Cached query response" }, "select": { "description": "Return a limited set of data. Required.", @@ -1647,6 +1621,33 @@ "required": ["kind", "select"], "type": "object" }, + "EventsQueryResponse": { + "additionalProperties": false, + "properties": { + "columns": { + "items": {}, + "type": "array" + }, + "hasMore": { + "type": "boolean" + }, + "results": { + "items": { + "items": {}, + "type": "array" + }, + "type": "array" + }, + "types": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": ["columns", "types", "results"], + "type": "object" + }, "FeaturePropertyFilter": { "additionalProperties": false, "properties": { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 5bfebc91f3c6c..161b0b349c4d1 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -134,6 +134,13 @@ export interface NewEntityNode extends EntityNode { event?: string | null } +export interface EventsQueryResponse { + columns: any[] + types: string[] + results: any[][] + hasMore?: boolean +} + export interface EventsQuery extends DataNode { kind: NodeKind.EventsQuery /** Return a limited set of data. Required. */ @@ -170,12 +177,7 @@ export interface EventsQuery extends DataNode { /** Columns to order by */ orderBy?: string[] - response?: { - columns: string[] - types: string[] - results: any[][] - hasMore?: boolean - } + response?: EventsQueryResponse } export interface PersonsNode extends DataNode { diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index afba1f5cb50d5..b43b558fccc23 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -1,6 +1,6 @@ import re from enum import Enum -from typing import Any, List, Literal +from typing import Any, List, Literal, Optional, Union from pydantic import BaseModel, Extra @@ -102,3 +102,29 @@ class Placeholder(Expr): class Call(Expr): name: str args: List[Expr] + + +class JoinExpr(Expr): + table: Optional[Union["SelectQuery", Field]] = None + table_final: Optional[bool] = None + alias: Optional[str] = None + join_type: Optional[str] = None + join_constraint: Optional[Expr] = None + join_expr: Optional["JoinExpr"] = None + + +class SelectQuery(Expr): + select: List[Expr] + select_from: Optional[JoinExpr] = None + where: Optional[Expr] = None + prewhere: Optional[Expr] = None + having: Optional[Expr] = None + group_by: Optional[List[Expr]] = None + order_by: Optional[List[OrderExpr]] = None + limit: Optional[int] = None + offset: Optional[int] = None + distinct: Optional[bool] = None + + +JoinExpr.update_forward_refs(SelectQuery=SelectQuery) +JoinExpr.update_forward_refs(JoinExpr=JoinExpr) diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 2f2359c104562..a7f7a8357c7ff 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -124,3 +124,6 @@ "person.created_at", "person.properties", ] + +# Never return more rows than this in top level HogQL SELECT statements +MAX_SELECT_RETURNED_ROWS = 65535 diff --git a/posthog/hogql/context.py b/posthog/hogql/context.py index 6e2221a3d7348..df3aabbd97e12 100644 --- a/posthog/hogql/context.py +++ b/posthog/hogql/context.py @@ -20,4 +20,9 @@ class HogQLContext: field_access_logs: List[HogQLFieldAccess] = field(default_factory=list) # Did the last calls to translate_hogql since setting these to False contain any of the following found_aggregation: bool = False + # Do we need to join the persons table or not using_person_on_events: bool = True + # If set, allows printing full SELECT queries in ClickHouse + select_team_id: Optional[int] = None + # Do we apply a limit of MAX_SELECT_RETURNED_ROWS=65535 to the topmost select query? + limit_top_select: bool = True diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index bc3eca9e61c15..b1891dddad6fe 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional +from typing import Dict, Literal, Optional, cast from antlr4 import CommonTokenStream, InputStream, ParseTreeVisitor from antlr4.error.ErrorListener import ErrorListener @@ -18,6 +18,14 @@ def parse_expr(expr: str, placeholders: Optional[Dict[str, ast.Expr]] = None) -> return node +def parse_select(statement: str, placeholders: Optional[Dict[str, ast.Expr]] = None) -> ast.Expr: + parse_tree = get_parser(statement).select() + node = HogQLParseTreeConverter().visit(parse_tree) + if placeholders: + node = replace_placeholders(node, placeholders) + return node + + def get_parser(query: str) -> HogQLParser: input_stream = InputStream(data=query) lexer = HogQLLexer(input_stream) @@ -35,16 +43,69 @@ def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): class HogQLParseTreeConverter(ParseTreeVisitor): def visitSelect(self, ctx: HogQLParser.SelectContext): - raise NotImplementedError(f"Unsupported node: SelectQuery") + return self.visit(ctx.selectUnionStmt() or ctx.selectStmt()) def visitSelectUnionStmt(self, ctx: HogQLParser.SelectUnionStmtContext): - raise NotImplementedError(f"Unsupported node: SelectUnionStmt") + selects = ctx.selectStmtWithParens() + if len(selects) != 1: + raise NotImplementedError(f"Unsupported: UNION ALL") + return self.visit(selects[0]) def visitSelectStmtWithParens(self, ctx: HogQLParser.SelectStmtWithParensContext): - raise NotImplementedError(f"Unsupported node: SelectStmtWithParens") + return self.visit(ctx.selectStmt() or ctx.selectUnionStmt()) def visitSelectStmt(self, ctx: HogQLParser.SelectStmtContext): - raise NotImplementedError(f"Unsupported node: SelectStmt") + select = self.visit(ctx.columnExprList()) if ctx.columnExprList() else [] + select_from = self.visit(ctx.fromClause()) if ctx.fromClause() else None + where = self.visit(ctx.whereClause()) if ctx.whereClause() else None + prewhere = self.visit(ctx.prewhereClause()) if ctx.prewhereClause() else None + having = self.visit(ctx.havingClause()) if ctx.havingClause() else None + group_by = self.visit(ctx.groupByClause()) if ctx.groupByClause() else None + order_by = self.visit(ctx.orderByClause()) if ctx.orderByClause() else None + + limit = None + offset = None + if ctx.limitClause() and ctx.limitClause().limitExpr(): + limit_expr = ctx.limitClause().limitExpr() + limit_node = self.visit(limit_expr.columnExpr(0)) + if limit_node is not None: + if isinstance(limit_node, ast.Constant) and isinstance(limit_node.value, int): + limit = limit_node.value + else: + raise Exception(f"LIMIT must be an integer") + if limit_expr.columnExpr(1): + offset_node = self.visit(limit_expr.columnExpr(1)) + if offset_node is not None: + if isinstance(offset_node, ast.Constant) and isinstance(offset_node.value, int): + offset = offset_node.value + else: + raise Exception(f"OFFSET must be an integer") + + if ctx.withClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.withClause()") + if ctx.topClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.topClause()") + if ctx.arrayJoinClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.arrayJoinClause()") + if ctx.windowClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.windowClause()") + if ctx.limitByClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.limitByClause()") + if ctx.settingsClause(): + raise NotImplementedError(f"Unsupported: SelectStmt.settingsClause()") + + return ast.SelectQuery( + select=select, + distinct=True if ctx.DISTINCT() else None, + select_from=select_from, + where=where, + prewhere=prewhere, + having=having, + group_by=group_by, + order_by=order_by, + limit=limit, + offset=offset, + ) def visitWithClause(self, ctx: HogQLParser.WithClauseContext): raise NotImplementedError(f"Unsupported node: WithClause") @@ -53,7 +114,7 @@ def visitTopClause(self, ctx: HogQLParser.TopClauseContext): raise NotImplementedError(f"Unsupported node: TopClause") def visitFromClause(self, ctx: HogQLParser.FromClauseContext): - raise NotImplementedError(f"Unsupported node: FromClause") + return self.visit(ctx.joinExpr()) def visitArrayJoinClause(self, ctx: HogQLParser.ArrayJoinClauseContext): raise NotImplementedError(f"Unsupported node: ArrayJoinClause") @@ -62,19 +123,19 @@ def visitWindowClause(self, ctx: HogQLParser.WindowClauseContext): raise NotImplementedError(f"Unsupported node: WindowClause") def visitPrewhereClause(self, ctx: HogQLParser.PrewhereClauseContext): - raise NotImplementedError(f"Unsupported node: PrewhereClause") + return self.visit(ctx.columnExpr()) def visitWhereClause(self, ctx: HogQLParser.WhereClauseContext): - raise NotImplementedError(f"Unsupported node: WhereClause") + return self.visit(ctx.columnExpr()) def visitGroupByClause(self, ctx: HogQLParser.GroupByClauseContext): - raise NotImplementedError(f"Unsupported node: GroupByClause") + return self.visit(ctx.columnExprList()) def visitHavingClause(self, ctx: HogQLParser.HavingClauseContext): - raise NotImplementedError(f"Unsupported node: HavingClause") + return self.visit(ctx.columnExpr()) def visitOrderByClause(self, ctx: HogQLParser.OrderByClauseContext): - raise NotImplementedError(f"Unsupported node: OrderByClause") + return self.visit(ctx.orderExprList()) def visitProjectionOrderByClause(self, ctx: HogQLParser.ProjectionOrderByClauseContext): raise NotImplementedError(f"Unsupported node: ProjectionOrderByClause") @@ -89,31 +150,102 @@ def visitSettingsClause(self, ctx: HogQLParser.SettingsClauseContext): raise NotImplementedError(f"Unsupported node: SettingsClause") def visitJoinExprOp(self, ctx: HogQLParser.JoinExprOpContext): - raise NotImplementedError(f"Unsupported node: JoinExprOp") + if ctx.GLOBAL(): + raise NotImplementedError(f"Unsupported: GLOBAL JOIN") + if ctx.LOCAL(): + raise NotImplementedError(f"Unsupported: LOCAL JOIN") + + join1: ast.JoinExpr = self.visit(ctx.joinExpr(0)) + join2: ast.JoinExpr = self.visit(ctx.joinExpr(1)) + + if ctx.joinOp(): + join_type = f"{self.visit(ctx.joinOp())} JOIN" + else: + join_type = "JOIN" + join_constraint = self.visit(ctx.joinConstraintClause()) + + join_without_next_expr = join1 + while join_without_next_expr.join_expr: + join_without_next_expr = join_without_next_expr.join_expr + + join_without_next_expr.join_expr = join2 + join_without_next_expr.join_constraint = join_constraint + join_without_next_expr.join_type = join_type + return join1 def visitJoinExprTable(self, ctx: HogQLParser.JoinExprTableContext): - raise NotImplementedError(f"Unsupported node: JoinExprTable") + if ctx.sampleClause(): + raise NotImplementedError(f"Unsupported: SAMPLE (JoinExprTable.sampleClause)") + table = self.visit(ctx.tableExpr()) + table_final = True if ctx.FINAL() else None + if isinstance(table, ast.JoinExpr): + # visitTableExprAlias returns a JoinExpr to pass the alias + table.table_final = table_final + return table + return ast.JoinExpr(table=table, table_final=table_final) def visitJoinExprParens(self, ctx: HogQLParser.JoinExprParensContext): - raise NotImplementedError(f"Unsupported node: JoinExprParens") + return self.visit(ctx.joinExpr()) def visitJoinExprCrossOp(self, ctx: HogQLParser.JoinExprCrossOpContext): raise NotImplementedError(f"Unsupported node: JoinExprCrossOp") def visitJoinOpInner(self, ctx: HogQLParser.JoinOpInnerContext): - raise NotImplementedError(f"Unsupported node: JoinOpInner") + tokens = [] + if ctx.LEFT(): + tokens.append("INNER") + if ctx.ALL(): + tokens.append("ALL") + if ctx.ANTI(): + tokens.append("ANTI") + if ctx.ANY(): + tokens.append("ANY") + if ctx.ASOF(): + tokens.append("ASOF") + return " ".join(tokens) def visitJoinOpLeftRight(self, ctx: HogQLParser.JoinOpLeftRightContext): - raise NotImplementedError(f"Unsupported node: JoinOpLeftRight") + tokens = [] + if ctx.LEFT(): + tokens.append("LEFT") + if ctx.RIGHT(): + tokens.append("RIGHT") + if ctx.OUTER(): + tokens.append("OUTER") + if ctx.SEMI(): + tokens.append("SEMI") + if ctx.ALL(): + tokens.append("ALL") + if ctx.ANTI(): + tokens.append("ANTI") + if ctx.ANY(): + tokens.append("ANY") + if ctx.ASOF(): + tokens.append("ASOF") + return " ".join(tokens) def visitJoinOpFull(self, ctx: HogQLParser.JoinOpFullContext): - raise NotImplementedError(f"Unsupported node: JoinOpFull") + tokens = [] + if ctx.LEFT(): + tokens.append("FULL") + if ctx.OUTER(): + tokens.append("OUTER") + if ctx.ALL(): + tokens.append("ALL") + if ctx.ANY(): + tokens.append("ANY") + return " ".join(tokens) def visitJoinOpCross(self, ctx: HogQLParser.JoinOpCrossContext): raise NotImplementedError(f"Unsupported node: JoinOpCross") def visitJoinConstraintClause(self, ctx: HogQLParser.JoinConstraintClauseContext): - raise NotImplementedError(f"Unsupported node: JoinConstraintClause") + if ctx.USING(): + raise NotImplementedError(f"Unsupported: JOIN ... USING") + column_expr_list = self.visit(ctx.columnExprList()) + if len(column_expr_list) != 1: + raise NotImplementedError(f"Unsupported: JOIN ... ON with multiple expressions") + return column_expr_list[0] def visitSampleClause(self, ctx: HogQLParser.SampleClauseContext): raise NotImplementedError(f"Unsupported node: SampleClause") @@ -122,10 +254,11 @@ def visitLimitExpr(self, ctx: HogQLParser.LimitExprContext): raise NotImplementedError(f"Unsupported node: LimitExpr") def visitOrderExprList(self, ctx: HogQLParser.OrderExprListContext): - raise NotImplementedError(f"Unsupported node: OrderExprList") + return [self.visit(expr) for expr in ctx.orderExpr()] def visitOrderExpr(self, ctx: HogQLParser.OrderExprContext): - raise NotImplementedError(f"Unsupported node: OrderExpr") + order = "DESC" if ctx.DESC() or ctx.DESCENDING() else "ASC" + return ast.OrderExpr(expr=self.visit(ctx.columnExpr()), order=cast(Literal["ASC", "DESC"], order)) def visitRatioExpr(self, ctx: HogQLParser.RatioExprContext): raise NotImplementedError(f"Unsupported node: RatioExpr") @@ -209,7 +342,7 @@ def visitColumnExprNegate(self, ctx: HogQLParser.ColumnExprNegateContext): raise NotImplementedError(f"Unsupported node: ColumnExprNegate") def visitColumnExprSubquery(self, ctx: HogQLParser.ColumnExprSubqueryContext): - raise NotImplementedError(f"Unsupported node: ColumnExprSubquery") + return self.visit(ctx.selectUnionStmt()) def visitColumnExprLiteral(self, ctx: HogQLParser.ColumnExprLiteralContext): return self.visitChildren(ctx) @@ -410,17 +543,17 @@ def visitColumnIdentifier(self, ctx: HogQLParser.ColumnIdentifierContext): return ast.Field(chain=table + nested) def visitNestedIdentifier(self, ctx: HogQLParser.NestedIdentifierContext): - chain = [self.visit(identifier) for identifier in ctx.identifier()] - return chain + return [self.visit(identifier) for identifier in ctx.identifier()] def visitTableExprIdentifier(self, ctx: HogQLParser.TableExprIdentifierContext): - raise NotImplementedError(f"Unsupported node: TableExprIdentifier") + chain = self.visit(ctx.tableIdentifier()) + return ast.Field(chain=chain) def visitTableExprSubquery(self, ctx: HogQLParser.TableExprSubqueryContext): - raise NotImplementedError(f"Unsupported node: TableExprSubquery") + return self.visit(ctx.selectUnionStmt()) def visitTableExprAlias(self, ctx: HogQLParser.TableExprAliasContext): - raise NotImplementedError(f"Unsupported node: TableExprAlias") + return ast.JoinExpr(table=self.visit(ctx.tableExpr()), alias=self.visit(ctx.alias() or ctx.identifier())) def visitTableExprFunction(self, ctx: HogQLParser.TableExprFunctionContext): raise NotImplementedError(f"Unsupported node: TableExprFunction") diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 61c2ee7020d1b..dfba9de265867 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -7,6 +7,7 @@ EVENT_PERSON_FIELDS, HOGQL_AGGREGATIONS, KEYWORDS, + MAX_SELECT_RETURNED_ROWS, SELECT_STAR_FROM_EVENTS_FIELDS, ) from posthog.hogql.context import HogQLContext, HogQLFieldAccess @@ -14,13 +15,83 @@ from posthog.hogql.print_string import print_hogql_identifier +def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: + """Add a mandatory "and(team_id, ...)" filter around the expression.""" + if not context.select_team_id: + raise ValueError("context.select_team_id not found") + + from posthog.hogql.parser import parse_expr + + team_clause = parse_expr("team_id = {team_id}", {"team_id": ast.Constant(value=context.select_team_id)}) + if isinstance(where, ast.And): + where = ast.And(exprs=[team_clause] + where.exprs) + elif where: + where = ast.And(exprs=[team_clause, where]) + else: + where = team_clause + return where + + def print_ast( node: ast.AST, stack: List[ast.AST], context: HogQLContext, dialect: Literal["hogql", "clickhouse"] ) -> str: """Translate a parsed HogQL expression in the shape of a Python AST into a Clickhouse expression.""" stack.append(node) - if isinstance(node, ast.BinaryOperation): + if isinstance(node, ast.SelectQuery): + if dialect == "clickhouse" and not context.select_team_id: + raise ValueError("Full SELECT queries are disabled if select_team_id is not set") + + columns = [print_ast(column, stack, context, dialect) for column in node.select] if node.select else ["1"] + + from_table = None + if node.select_from: + if node.select_from.alias is not None: + raise ValueError("Table aliases not yet supported") + if isinstance(node.select_from.table, ast.Field): + if node.select_from.table.chain != ["events"]: + raise ValueError('Only selecting from the "events" table is supported') + from_table = "events" + elif isinstance(node.select_from.table, ast.SelectQuery): + from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + else: + raise ValueError("Only selecting from a table or a subquery is supported") + + where = node.where + # Guard with team_id if selecting from the events table and printing ClickHouse SQL + # We do this in the printer, and not in a separate step, to be really sure this gets added. + if dialect == "clickhouse" and from_table == "events": + where = guard_where_team_id(where, context) + where = print_ast(where, stack, context, dialect) if where else None + + having = print_ast(node.having, stack, context, dialect) if node.having else None + prewhere = print_ast(node.prewhere, stack, context, dialect) if node.prewhere else None + group_by = [print_ast(column, stack, context, dialect) for column in node.group_by] if node.group_by else None + order_by = [print_ast(column, stack, context, dialect) for column in node.order_by] if node.order_by else None + + limit = node.limit + if context.limit_top_select: + if limit is not None: + limit = max(0, min(node.limit, MAX_SELECT_RETURNED_ROWS)) + if len(stack) == 1 and limit is None: + limit = MAX_SELECT_RETURNED_ROWS + + clauses = [ + f"SELECT {'DISTINCT ' if node.distinct else ''}{', '.join(columns)}", + f"FROM {from_table}" if from_table else None, + "WHERE " + where if where else None, + f"GROUP BY {', '.join(group_by)}" if group_by and len(group_by) > 0 else None, + "HAVING " + having if having else None, + "PREWHERE " + prewhere if prewhere else None, + f"ORDER BY {', '.join(order_by)}" if order_by and len(order_by) > 0 else None, + f"LIMIT {limit}" if limit is not None else None, + f"OFFSET {node.offset}" if node.offset is not None else None, + ] + response = " ".join([clause for clause in clauses if clause]) + if len(stack) > 1: + response = f"({response})" + + elif isinstance(node, ast.BinaryOperation): if node.op == ast.BinaryOperationType.Add: response = f"plus({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" elif node.op == ast.BinaryOperationType.Sub: diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index dfe0d7b169d36..2c6b34a4e5719 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -1,5 +1,5 @@ from posthog.hogql import ast -from posthog.hogql.parser import parse_expr +from posthog.hogql.parser import parse_expr, parse_select from posthog.test.base import BaseTest @@ -354,3 +354,239 @@ def test_placeholders(self): right=ast.Constant(value=123), ), ) + + def test_select_columns(self): + self.assertEqual(parse_select("select 1"), ast.SelectQuery(select=[ast.Constant(value=1)])) + self.assertEqual( + parse_select("select 1, 4, 'string'"), + ast.SelectQuery(select=[ast.Constant(value=1), ast.Constant(value=4), ast.Constant(value="string")]), + ) + + def test_select_columns_distinct(self): + self.assertEqual( + parse_select("select distinct 1"), ast.SelectQuery(select=[ast.Constant(value=1)], distinct=True) + ) + + def test_select_where(self): + self.assertEqual( + parse_select("select 1 where true"), + ast.SelectQuery(select=[ast.Constant(value=1)], where=ast.Constant(value=True)), + ) + self.assertEqual( + parse_select("select 1 where 1 == 2"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + where=ast.CompareOperation( + op=ast.CompareOperationType.Eq, left=ast.Constant(value=1), right=ast.Constant(value=2) + ), + ), + ) + + def test_select_prewhere(self): + self.assertEqual( + parse_select("select 1 prewhere true"), + ast.SelectQuery(select=[ast.Constant(value=1)], prewhere=ast.Constant(value=True)), + ) + self.assertEqual( + parse_select("select 1 prewhere 1 == 2"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + prewhere=ast.CompareOperation( + op=ast.CompareOperationType.Eq, left=ast.Constant(value=1), right=ast.Constant(value=2) + ), + ), + ) + + def test_select_having(self): + self.assertEqual( + parse_select("select 1 having true"), + ast.SelectQuery(select=[ast.Constant(value=1)], having=ast.Constant(value=True)), + ) + self.assertEqual( + parse_select("select 1 having 1 == 2"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + having=ast.CompareOperation( + op=ast.CompareOperationType.Eq, left=ast.Constant(value=1), right=ast.Constant(value=2) + ), + ), + ) + + def test_select_complex_wheres(self): + self.assertEqual( + parse_select("select 1 prewhere 2 != 3 where 1 == 2 having 'string' like '%a%'"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + where=ast.CompareOperation( + op=ast.CompareOperationType.Eq, left=ast.Constant(value=1), right=ast.Constant(value=2) + ), + prewhere=ast.CompareOperation( + op=ast.CompareOperationType.NotEq, left=ast.Constant(value=2), right=ast.Constant(value=3) + ), + having=ast.CompareOperation( + op=ast.CompareOperationType.Like, left=ast.Constant(value="string"), right=ast.Constant(value="%a%") + ), + ), + ) + + def test_select_from(self): + self.assertEqual( + parse_select("select 1 from events"), + ast.SelectQuery( + select=[ast.Constant(value=1)], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])) + ), + ) + self.assertEqual( + parse_select("select 1 from events as e"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"]), alias="e"), + ), + ) + self.assertEqual( + parse_select("select 1 from complex.table"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["complex", "table"])), + ), + ) + self.assertEqual( + parse_select("select 1 from complex.table as a"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["complex", "table"]), alias="a"), + ), + ) + self.assertEqual( + parse_select("select 1 from (select 1 from events)"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr( + table=ast.SelectQuery( + select=[ast.Constant(value=1)], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])) + ) + ), + ), + ) + self.assertEqual( + parse_select("select 1 from (select 1 from events) as sq"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr( + table=ast.SelectQuery( + select=[ast.Constant(value=1)], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])) + ), + alias="sq", + ), + ), + ) + + def test_select_from_join(self): + self.assertEqual( + parse_select("select 1 from events JOIN events2 ON 1"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"]), + join_type="JOIN", + join_constraint=ast.Constant(value=1), + join_expr=ast.JoinExpr(table=ast.Field(chain=["events2"])), + ), + ), + ) + self.assertEqual( + parse_select("select * from events LEFT OUTER JOIN events2 ON 1"), + ast.SelectQuery( + select=[ast.Field(chain=["*"])], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"]), + join_type="LEFT OUTER JOIN", + join_constraint=ast.Constant(value=1), + join_expr=ast.JoinExpr(table=ast.Field(chain=["events2"])), + ), + ), + ) + self.assertEqual( + parse_select("select 1 from events LEFT OUTER JOIN events2 ON 1 ANY RIGHT JOIN events3 ON 2"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"]), + join_type="LEFT OUTER JOIN", + join_constraint=ast.Constant(value=1), + join_expr=ast.JoinExpr( + table=ast.Field(chain=["events2"]), + join_type="RIGHT ANY JOIN", + join_constraint=ast.Constant(value=2), + join_expr=ast.JoinExpr(table=ast.Field(chain=["events3"])), + ), + ), + ), + ) + + def test_select_group_by(self): + self.assertEqual( + parse_select("select 1 from events GROUP BY 1, event"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + group_by=[ast.Constant(value=1), ast.Field(chain=["event"])], + ), + ) + + def test_select_order_by(self): + self.assertEqual( + parse_select("select 1 from events ORDER BY 1 ASC, event, timestamp DESC"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + order_by=[ + ast.OrderExpr(expr=ast.Constant(value=1), order="ASC"), + ast.OrderExpr(expr=ast.Field(chain=["event"]), order="ASC"), + ast.OrderExpr(expr=ast.Field(chain=["timestamp"]), order="DESC"), + ], + ), + ) + + def test_select_limit_offset(self): + self.assertEqual( + parse_select("select 1 from events LIMIT 1"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + limit=1, + ), + ) + self.assertEqual( + parse_select("select 1 from events LIMIT 1 OFFSET 3"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + limit=1, + offset=3, + ), + ) + + def test_select_placeholders(self): + self.assertEqual( + parse_select("select 1 where 1 == {hogql_val_1}"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + where=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Constant(value=1), + right=ast.Placeholder(field="hogql_val_1"), + ), + ), + ) + self.assertEqual( + parse_select("select 1 where 1 == {hogql_val_1}", {"hogql_val_1": ast.Constant(value="bar")}), + ast.SelectQuery( + select=[ast.Constant(value=1)], + where=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Constant(value=1), + right=ast.Constant(value="bar"), + ), + ), + ) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 80e15dcb5f3d2..ded3198704c69 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -13,6 +13,12 @@ def _expr( ) -> str: return translate_hogql(query, context or HogQLContext(), dialect) + # Helper to always translate HogQL with a blank context, + def _select( + self, query: str, context: Optional[HogQLContext] = None, dialect: Literal["hogql", "clickhouse"] = "clickhouse" + ) -> str: + return translate_hogql(query, context or HogQLContext(select_team_id=42), dialect) + def _assert_expr_error(self, expr, expected_error, dialect: Literal["hogql", "clickhouse"] = "clickhouse"): with self.assertRaises(ValueError) as context: self._expr(expr, None, dialect) @@ -20,6 +26,13 @@ def _assert_expr_error(self, expr, expected_error, dialect: Literal["hogql", "cl raise AssertionError(f"Expected '{expected_error}' in '{str(context.exception)}'") self.assertTrue(expected_error in str(context.exception)) + def _assert_select_error(self, statement, expected_error, dialect: Literal["hogql", "clickhouse"] = "clickhouse"): + with self.assertRaises(ValueError) as context: + self._select(statement, None, dialect) + if expected_error not in str(context.exception): + raise AssertionError(f"Expected '{expected_error}' in '{str(context.exception)}'") + self.assertTrue(expected_error in str(context.exception)) + def test_literals(self): self.assertEqual(self._expr("1 + 2"), "plus(1, 2)") self.assertEqual(self._expr("-1 + 2"), "plus(-1, 2)") @@ -330,3 +343,87 @@ def test_values(self): def test_no_alias_yet(self): self._assert_expr_error("1 as team_id", "Unknown AST node Alias") self._assert_expr_error("1 as `-- select team_id`", "Unknown AST node Alias") + + def test_select(self): + self.assertEqual(self._select("select 1"), "SELECT 1 LIMIT 65535") + self.assertEqual(self._select("select 1 + 2"), "SELECT plus(1, 2) LIMIT 65535") + self.assertEqual(self._select("select 1 + 2, 3"), "SELECT plus(1, 2), 3 LIMIT 65535") + self.assertEqual( + self._select("select 1 + 2, 3 + 4 from events"), + "SELECT plus(1, 2), plus(3, 4) FROM events WHERE equals(team_id, 42) LIMIT 65535", + ) + + def test_select_alias(self): + # currently not supported! + self._assert_select_error("select 1 as b", "Unknown AST node Alias") + self._assert_select_error("select 1 from events as e", "Table aliases not yet supported") + + def test_select_from(self): + self.assertEqual( + self._select("select 1 from events"), "SELECT 1 FROM events WHERE equals(team_id, 42) LIMIT 65535" + ) + self._assert_select_error("select 1 from other", 'Only selecting from the "events" table is supported') + + def test_select_where(self): + self.assertEqual( + self._select("select 1 from events where 1 == 2"), + "SELECT 1 FROM events WHERE and(equals(team_id, 42), equals(1, 2)) LIMIT 65535", + ) + + def test_select_having(self): + self.assertEqual( + self._select("select 1 from events having 1 == 2"), + "SELECT 1 FROM events WHERE equals(team_id, 42) HAVING equals(1, 2) LIMIT 65535", + ) + + def test_select_prewhere(self): + self.assertEqual( + self._select("select 1 from events prewhere 1 == 2"), + "SELECT 1 FROM events WHERE equals(team_id, 42) PREWHERE equals(1, 2) LIMIT 65535", + ) + + def test_select_order_by(self): + self.assertEqual( + self._select("select event from events order by event"), + "SELECT event FROM events WHERE equals(team_id, 42) ORDER BY event ASC LIMIT 65535", + ) + self.assertEqual( + self._select("select event from events order by event desc"), + "SELECT event FROM events WHERE equals(team_id, 42) ORDER BY event DESC LIMIT 65535", + ) + self.assertEqual( + self._select("select event from events order by event desc, timestamp"), + "SELECT event FROM events WHERE equals(team_id, 42) ORDER BY event DESC, timestamp ASC LIMIT 65535", + ) + + def test_select_limit(self): + self.assertEqual( + self._select("select event from events limit 10"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10", + ) + self.assertEqual( + self._select("select event from events limit 10000000"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 65535", + ) + + def test_select_offset(self): + self.assertEqual( + self._select("select event from events limit 10 offset 10"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10 OFFSET 10", + ) + self.assertEqual( + self._select("select event from events limit 10 offset 0"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10 OFFSET 0", + ) + + def test_select_group_by(self): + self.assertEqual( + self._select("select event from events group by event, timestamp"), + "SELECT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp LIMIT 65535", + ) + + def test_select_distinct(self): + self.assertEqual( + self._select("select distinct event from events group by event, timestamp"), + "SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp LIMIT 65535", + ) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index 847369a2fc951..878bf00e4ed32 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -4,7 +4,6 @@ from dateutil.parser import isoparse from django.utils.timezone import now -from pydantic import BaseModel from posthog.api.utils import get_pk_or_uuid from posthog.clickhouse.client.connection import Workload @@ -22,7 +21,7 @@ from posthog.models.event.util import ElementSerializer from posthog.models.property.util import parse_prop_grouped_clauses from posthog.queries.insight import insight_query_with_columns, insight_sync_execute -from posthog.schema import EventsQuery +from posthog.schema import EventsQuery, EventsQueryResponse from posthog.utils import relative_date_parse # Return at most this number of events in CSV export @@ -31,13 +30,6 @@ QUERY_MAXIMUM_LIMIT = 100_000 -class EventsQueryResponse(BaseModel): - columns: List[str] - types: List[str] - results: List[List] - hasMore: bool - - def determine_event_conditions(conditions: Dict[str, Union[None, str, List[str]]]) -> Tuple[str, Dict]: result = "" params: Dict[str, Union[str, List[str]]] = {} diff --git a/posthog/schema.py b/posthog/schema.py index 28bad7dd7ad5e..a9c22bf6e7321 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -176,11 +176,11 @@ class Config: results: List[EventType] -class Response1(BaseModel): +class EventsQueryResponse(BaseModel): class Config: extra = Extra.forbid - columns: List[str] + columns: List hasMore: Optional[bool] = None results: List[List] types: List[str] @@ -668,7 +668,7 @@ class Config: ] ] ] = Field(None, description="Properties configurable in the interface") - response: Optional[Response1] = Field(None, description="Cached query response") + response: Optional[EventsQueryResponse] = Field(None, description="Cached query response") select: List[str] = Field(..., description="Return a limited set of data. Required.") where: Optional[List[str]] = Field(None, description="HogQL filters to apply on returned data") From a87115f5fa42eb0fa7441fbae6a7b3033c6ed7bf Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 15:16:35 +0100 Subject: [PATCH 002/142] visitor --- posthog/hogql/hogql.py | 7 +++++-- posthog/hogql/test/test_visitor.py | 24 ++++++++++++++++++++++++ posthog/hogql/visitor.py | 26 ++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 81257157e08db..dc16b3cf8e9f6 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -1,7 +1,7 @@ from typing import Literal from posthog.hogql.context import HogQLContext -from posthog.hogql.parser import parse_expr +from posthog.hogql.parser import parse_expr, parse_select from posthog.hogql.printer import print_ast @@ -11,7 +11,10 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", raise ValueError("Empty query") try: - node = parse_expr(query) + if context.select_team_id: + node = parse_select(query) + else: + node = parse_expr(query) except SyntaxError as err: raise ValueError(f"SyntaxError: {err.msg}") except NotImplementedError as err: diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index 42857388f890d..f60812f53aab8 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -65,6 +65,30 @@ def test_everything_visitor(self): ], ) ), + ast.Alias(expr=ast.SelectQuery(select=[ast.Field(chain=["timestamp"])]), alias="f"), + ast.SelectQuery( + select=[ast.Field(chain=["a"])], + select_from=ast.JoinExpr( + table=ast.Field(chain=["b"]), + table_final=True, + alias="c", + join_type="INNER", + join_constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["d"]), + right=ast.Field(chain=["e"]), + ), + join_expr=ast.JoinExpr(table=ast.Field(chain=["f"])), + ), + where=ast.Constant(value=True), + prewhere=ast.Constant(value=True), + having=ast.Constant(value=True), + group_by=[ast.Constant(value=True)], + order_by=[ast.OrderExpr(expr=ast.Constant(value=True), order="DESC")], + limit=1, + offset=0, + distinct=True, + ), ] ) self.assertEqual(node, EverythingVisitor().visit(node)) diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index e36ec7d6af6e4..1bca4d05a78f4 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -3,6 +3,8 @@ class Visitor(object): def visit(self, node: ast.AST): + if node is None: + return node return node.accept(self) @@ -59,3 +61,27 @@ def visit_call(self, call: ast.Call): name=call.name, args=[self.visit(arg) for arg in call.args], ) + + def visit_join_expr(self, node: ast.JoinExpr): + return ast.JoinExpr( + table=self.visit(node.table), + join_expr=self.visit(node.join_expr), + table_final=node.table_final, + alias=node.alias, + join_type=node.join_type, + join_constraint=self.visit(node.join_constraint), + ) + + def visit_select_query(self, node: ast.SelectQuery): + return ast.SelectQuery( + select=[self.visit(expr) for expr in node.select] if node.select else None, + select_from=self.visit(node.select_from), + where=self.visit(node.where), + prewhere=self.visit(node.prewhere), + having=self.visit(node.having), + group_by=[self.visit(expr) for expr in node.group_by] if node.group_by else None, + order_by=[self.visit(expr) for expr in node.order_by] if node.order_by else None, + limit=node.limit, + offset=node.offset, + distinct=node.distinct, + ) From c55f170b9c26f3f6cf5329b0637b4fc30c9ac9ec Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 15:23:08 +0100 Subject: [PATCH 003/142] cleanup --- posthog/hogql/printer.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index dfba9de265867..d07599d080e21 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -20,8 +20,6 @@ def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: if not context.select_team_id: raise ValueError("context.select_team_id not found") - from posthog.hogql.parser import parse_expr - team_clause = parse_expr("team_id = {team_id}", {"team_id": ast.Constant(value=context.select_team_id)}) if isinstance(where, ast.And): where = ast.And(exprs=[team_clause] + where.exprs) @@ -58,9 +56,10 @@ def print_ast( raise ValueError("Only selecting from a table or a subquery is supported") where = node.where - # Guard with team_id if selecting from the events table and printing ClickHouse SQL + # Guard with team_id if selecting from a table and printing ClickHouse SQL # We do this in the printer, and not in a separate step, to be really sure this gets added. - if dialect == "clickhouse" and from_table == "events": + # This will be improved when we add proper table and column alias support. For now, let's just be safe. + if dialect == "clickhouse" and from_table is not None: where = guard_where_team_id(where, context) where = print_ast(where, stack, context, dialect) if where else None From d47bc616d9905a56ba540df096858cf5ce68f59b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 16:03:43 +0100 Subject: [PATCH 004/142] parse limit by --- posthog/hogql/ast.py | 8 ++-- posthog/hogql/parser.py | 64 ++++++++++++------------------- posthog/hogql/test/test_parser.py | 26 +++++++++++-- 3 files changed, 52 insertions(+), 46 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index b43b558fccc23..012a9b9a8b57a 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -115,15 +115,17 @@ class JoinExpr(Expr): class SelectQuery(Expr): select: List[Expr] + distinct: Optional[bool] = None select_from: Optional[JoinExpr] = None where: Optional[Expr] = None prewhere: Optional[Expr] = None having: Optional[Expr] = None group_by: Optional[List[Expr]] = None order_by: Optional[List[OrderExpr]] = None - limit: Optional[int] = None - offset: Optional[int] = None - distinct: Optional[bool] = None + limit: Optional[Expr] = None + limit_by: Optional[List[Expr]] = None + limit_with_ties: Optional[bool] = None + offset: Optional[Expr] = None JoinExpr.update_forward_refs(SelectQuery=SelectQuery) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index b1891dddad6fe..f393769960196 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -55,31 +55,28 @@ def visitSelectStmtWithParens(self, ctx: HogQLParser.SelectStmtWithParensContext return self.visit(ctx.selectStmt() or ctx.selectUnionStmt()) def visitSelectStmt(self, ctx: HogQLParser.SelectStmtContext): - select = self.visit(ctx.columnExprList()) if ctx.columnExprList() else [] - select_from = self.visit(ctx.fromClause()) if ctx.fromClause() else None - where = self.visit(ctx.whereClause()) if ctx.whereClause() else None - prewhere = self.visit(ctx.prewhereClause()) if ctx.prewhereClause() else None - having = self.visit(ctx.havingClause()) if ctx.havingClause() else None - group_by = self.visit(ctx.groupByClause()) if ctx.groupByClause() else None - order_by = self.visit(ctx.orderByClause()) if ctx.orderByClause() else None - - limit = None - offset = None - if ctx.limitClause() and ctx.limitClause().limitExpr(): - limit_expr = ctx.limitClause().limitExpr() - limit_node = self.visit(limit_expr.columnExpr(0)) - if limit_node is not None: - if isinstance(limit_node, ast.Constant) and isinstance(limit_node.value, int): - limit = limit_node.value - else: - raise Exception(f"LIMIT must be an integer") + select_query = ast.SelectQuery( + select=self.visit(ctx.columnExprList()) if ctx.columnExprList() else [], + distinct=True if ctx.DISTINCT() else None, + select_from=self.visit(ctx.fromClause()) if ctx.fromClause() else None, + where=self.visit(ctx.whereClause()) if ctx.whereClause() else None, + prewhere=self.visit(ctx.prewhereClause()) if ctx.prewhereClause() else None, + having=self.visit(ctx.havingClause()) if ctx.havingClause() else None, + group_by=self.visit(ctx.groupByClause()) if ctx.groupByClause() else None, + order_by=self.visit(ctx.orderByClause()) if ctx.orderByClause() else None, + ) + + any_limit_clause = ctx.limitClause() or ctx.limitByClause() + if any_limit_clause and any_limit_clause.limitExpr(): + limit_expr = any_limit_clause.limitExpr() + if limit_expr.columnExpr(0): + select_query.limit = self.visit(limit_expr.columnExpr(0)) if limit_expr.columnExpr(1): - offset_node = self.visit(limit_expr.columnExpr(1)) - if offset_node is not None: - if isinstance(offset_node, ast.Constant) and isinstance(offset_node.value, int): - offset = offset_node.value - else: - raise Exception(f"OFFSET must be an integer") + select_query.offset = self.visit(limit_expr.columnExpr(1)) + if ctx.limitClause() and ctx.limitClause().WITH() and ctx.limitClause().TIES(): + select_query.limit_with_ties = True + if ctx.limitByClause() and ctx.limitByClause().columnExprList(): + select_query.limit_by = self.visit(ctx.limitByClause().columnExprList()) if ctx.withClause(): raise NotImplementedError(f"Unsupported: SelectStmt.withClause()") @@ -89,23 +86,10 @@ def visitSelectStmt(self, ctx: HogQLParser.SelectStmtContext): raise NotImplementedError(f"Unsupported: SelectStmt.arrayJoinClause()") if ctx.windowClause(): raise NotImplementedError(f"Unsupported: SelectStmt.windowClause()") - if ctx.limitByClause(): - raise NotImplementedError(f"Unsupported: SelectStmt.limitByClause()") if ctx.settingsClause(): raise NotImplementedError(f"Unsupported: SelectStmt.settingsClause()") - return ast.SelectQuery( - select=select, - distinct=True if ctx.DISTINCT() else None, - select_from=select_from, - where=where, - prewhere=prewhere, - having=having, - group_by=group_by, - order_by=order_by, - limit=limit, - offset=offset, - ) + return select_query def visitWithClause(self, ctx: HogQLParser.WithClauseContext): raise NotImplementedError(f"Unsupported node: WithClause") @@ -141,10 +125,10 @@ def visitProjectionOrderByClause(self, ctx: HogQLParser.ProjectionOrderByClauseC raise NotImplementedError(f"Unsupported node: ProjectionOrderByClause") def visitLimitByClause(self, ctx: HogQLParser.LimitByClauseContext): - raise NotImplementedError(f"Unsupported node: LimitByClause") + raise Exception(f"Parsed as part of SelectStmt, can't parse directly.") def visitLimitClause(self, ctx: HogQLParser.LimitClauseContext): - raise NotImplementedError(f"Unsupported node: LimitClause") + raise Exception(f"Parsed as part of SelectStmt, can't parse directly.") def visitSettingsClause(self, ctx: HogQLParser.SettingsClauseContext): raise NotImplementedError(f"Unsupported node: SettingsClause") diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index 2c6b34a4e5719..55d943a1d197e 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -554,7 +554,7 @@ def test_select_limit_offset(self): ast.SelectQuery( select=[ast.Constant(value=1)], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), - limit=1, + limit=ast.Constant(value=1), ), ) self.assertEqual( @@ -562,8 +562,28 @@ def test_select_limit_offset(self): ast.SelectQuery( select=[ast.Constant(value=1)], select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), - limit=1, - offset=3, + limit=ast.Constant(value=1), + offset=ast.Constant(value=3), + ), + ) + self.assertEqual( + parse_select("select 1 from events LIMIT 1 OFFSET 3 WITH TIES"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + limit=ast.Constant(value=1), + limit_with_ties=True, + offset=ast.Constant(value=3), + ), + ) + self.assertEqual( + parse_select("select 1 from events LIMIT 1 OFFSET 3 BY 1, event"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), + limit=ast.Constant(value=1), + offset=ast.Constant(value=3), + limit_by=[ast.Constant(value=1), ast.Field(chain=["event"])], ), ) From 839d5059b60a3fb5efab513c44b84741d4f2675c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 16:16:18 +0100 Subject: [PATCH 005/142] parse limit by --- posthog/hogql/printer.py | 20 +++++++++++++++----- posthog/hogql/test/test_printer.py | 19 +++++++++++++++++++ posthog/hogql/test/test_visitor.py | 6 ++++-- posthog/hogql/visitor.py | 6 ++++-- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index d07599d080e21..e2ebe83dc4d23 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -71,9 +71,12 @@ def print_ast( limit = node.limit if context.limit_top_select: if limit is not None: - limit = max(0, min(node.limit, MAX_SELECT_RETURNED_ROWS)) - if len(stack) == 1 and limit is None: - limit = MAX_SELECT_RETURNED_ROWS + if isinstance(limit, ast.Constant) and isinstance(limit.value, int): + limit.value = min(limit.value, MAX_SELECT_RETURNED_ROWS) + else: + limit = ast.Call(name="min2", args=[ast.Constant(value=MAX_SELECT_RETURNED_ROWS), limit]) + elif len(stack) == 1: + limit = ast.Constant(value=MAX_SELECT_RETURNED_ROWS) clauses = [ f"SELECT {'DISTINCT ' if node.distinct else ''}{', '.join(columns)}", @@ -83,9 +86,16 @@ def print_ast( "HAVING " + having if having else None, "PREWHERE " + prewhere if prewhere else None, f"ORDER BY {', '.join(order_by)}" if order_by and len(order_by) > 0 else None, - f"LIMIT {limit}" if limit is not None else None, - f"OFFSET {node.offset}" if node.offset is not None else None, ] + if limit is not None: + clauses.append(f"LIMIT {print_ast(limit, stack, context, dialect)}"), + if node.offset is not None: + clauses.append(f"OFFSET {print_ast(node.offset, stack, context, dialect)}") + if node.limit_by is not None: + clauses.append(f"BY {', '.join([print_ast(expr, stack, context, dialect) for expr in node.limit_by])}") + if node.limit_with_ties: + clauses.append("WITH TIES") + response = " ".join([clause for clause in clauses if clause]) if len(stack) > 1: response = f"({response})" diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index ded3198704c69..e1e67e8106497 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -405,6 +405,15 @@ def test_select_limit(self): self._select("select event from events limit 10000000"), "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 65535", ) + self.assertEqual( + self._select("select event from events limit (select 1000000000)"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT min2(65535, (SELECT 1000000000))", + ) + + self.assertEqual( + self._select("select event from events limit (select 1000000000) with ties"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT min2(65535, (SELECT 1000000000)) WITH TIES", + ) def test_select_offset(self): self.assertEqual( @@ -415,6 +424,16 @@ def test_select_offset(self): self._select("select event from events limit 10 offset 0"), "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10 OFFSET 0", ) + self.assertEqual( + self._select("select event from events limit 10 offset 0 with ties"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10 OFFSET 0 WITH TIES", + ) + + def test_select_limit_by(self): + self.assertEqual( + self._select("select event from events limit 10 offset 0 by 1,event"), + "SELECT event FROM events WHERE equals(team_id, 42) LIMIT 10 OFFSET 0 BY 1, event", + ) def test_select_group_by(self): self.assertEqual( diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index f60812f53aab8..738b4ac3aed12 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -85,8 +85,10 @@ def test_everything_visitor(self): having=ast.Constant(value=True), group_by=[ast.Constant(value=True)], order_by=[ast.OrderExpr(expr=ast.Constant(value=True), order="DESC")], - limit=1, - offset=0, + limit=ast.Constant(value=1), + limit_by=[ast.Constant(value=True)], + limit_with_ties=True, + offset=ast.Or(exprs=[ast.Constant(value=1)]), distinct=True, ), ] diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 1bca4d05a78f4..66365f368bbc1 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -81,7 +81,9 @@ def visit_select_query(self, node: ast.SelectQuery): having=self.visit(node.having), group_by=[self.visit(expr) for expr in node.group_by] if node.group_by else None, order_by=[self.visit(expr) for expr in node.order_by] if node.order_by else None, - limit=node.limit, - offset=node.offset, + limit_by=[self.visit(expr) for expr in node.limit_by] if node.limit_by else None, + limit=self.visit(node.limit), + limit_with_ties=node.limit_with_ties, + offset=self.visit(node.offset), distinct=node.distinct, ) From fef94635ccaa2fc29568f8b11d15e21d4fb3d668 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 16:55:45 +0100 Subject: [PATCH 006/142] merge limit clauses --- posthog/hogql/grammar/HogQLParser.g4 | 4 +- posthog/hogql/grammar/HogQLParser.interp | 3 +- posthog/hogql/grammar/HogQLParser.py | 2164 +++++++++---------- posthog/hogql/grammar/HogQLParserVisitor.py | 5 - posthog/hogql/parser.py | 17 +- posthog/hogql/printer.py | 2 +- 6 files changed, 1063 insertions(+), 1132 deletions(-) diff --git a/posthog/hogql/grammar/HogQLParser.g4 b/posthog/hogql/grammar/HogQLParser.g4 index a2726834f1bca..757cb726f5ce7 100644 --- a/posthog/hogql/grammar/HogQLParser.g4 +++ b/posthog/hogql/grammar/HogQLParser.g4 @@ -21,7 +21,6 @@ selectStmt: groupByClause? (WITH (CUBE | ROLLUP))? (WITH TOTALS)? havingClause? orderByClause? - limitByClause? limitClause? settingsClause? ; @@ -37,8 +36,7 @@ groupByClause: GROUP BY ((CUBE | ROLLUP) LPAREN columnExprList RPAREN | columnEx havingClause: HAVING columnExpr; orderByClause: ORDER BY orderExprList; projectionOrderByClause: ORDER BY columnExprList; -limitByClause: LIMIT limitExpr BY columnExprList; -limitClause: LIMIT limitExpr (WITH TIES)?; +limitClause: LIMIT limitExpr ((WITH TIES) | BY columnExprList)?; settingsClause: SETTINGS settingExprList; joinExpr diff --git a/posthog/hogql/grammar/HogQLParser.interp b/posthog/hogql/grammar/HogQLParser.interp index a48298cae9931..e9aab592cff5b 100644 --- a/posthog/hogql/grammar/HogQLParser.interp +++ b/posthog/hogql/grammar/HogQLParser.interp @@ -488,7 +488,6 @@ groupByClause havingClause orderByClause projectionOrderByClause -limitByClause limitClause settingsClause joinExpr @@ -537,4 +536,4 @@ enumValue atn: -[4, 1, 234, 891, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 2, 60, 7, 60, 1, 0, 1, 0, 3, 0, 125, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 133, 8, 1, 10, 1, 12, 1, 136, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 143, 8, 2, 1, 3, 3, 3, 146, 8, 3, 1, 3, 1, 3, 3, 3, 150, 8, 3, 1, 3, 3, 3, 153, 8, 3, 1, 3, 1, 3, 3, 3, 157, 8, 3, 1, 3, 3, 3, 160, 8, 3, 1, 3, 3, 3, 163, 8, 3, 1, 3, 3, 3, 166, 8, 3, 1, 3, 3, 3, 169, 8, 3, 1, 3, 3, 3, 172, 8, 3, 1, 3, 1, 3, 3, 3, 176, 8, 3, 1, 3, 1, 3, 3, 3, 180, 8, 3, 1, 3, 3, 3, 183, 8, 3, 1, 3, 3, 3, 186, 8, 3, 1, 3, 3, 3, 189, 8, 3, 1, 3, 3, 3, 192, 8, 3, 1, 3, 3, 3, 195, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 204, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 3, 7, 210, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 237, 8, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 16, 1, 16, 1, 16, 1, 16, 3, 16, 259, 8, 16, 1, 17, 1, 17, 1, 17, 1, 18, 1, 18, 1, 18, 3, 18, 267, 8, 18, 1, 18, 3, 18, 270, 8, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 276, 8, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 1, 18, 3, 18, 284, 8, 18, 1, 18, 3, 18, 287, 8, 18, 1, 18, 1, 18, 1, 18, 1, 18, 5, 18, 293, 8, 18, 10, 18, 12, 18, 296, 9, 18, 1, 19, 3, 19, 299, 8, 19, 1, 19, 1, 19, 1, 19, 3, 19, 304, 8, 19, 1, 19, 3, 19, 307, 8, 19, 1, 19, 3, 19, 310, 8, 19, 1, 19, 1, 19, 3, 19, 314, 8, 19, 1, 19, 1, 19, 3, 19, 318, 8, 19, 1, 19, 3, 19, 321, 8, 19, 3, 19, 323, 8, 19, 1, 19, 3, 19, 326, 8, 19, 1, 19, 1, 19, 3, 19, 330, 8, 19, 1, 19, 1, 19, 3, 19, 334, 8, 19, 1, 19, 3, 19, 337, 8, 19, 3, 19, 339, 8, 19, 3, 19, 341, 8, 19, 1, 20, 3, 20, 344, 8, 20, 1, 20, 1, 20, 1, 20, 3, 20, 349, 8, 20, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 360, 8, 21, 1, 22, 1, 22, 1, 22, 1, 22, 3, 22, 366, 8, 22, 1, 23, 1, 23, 1, 23, 3, 23, 371, 8, 23, 1, 24, 1, 24, 1, 24, 5, 24, 376, 8, 24, 10, 24, 12, 24, 379, 9, 24, 1, 25, 1, 25, 3, 25, 383, 8, 25, 1, 25, 1, 25, 3, 25, 387, 8, 25, 1, 25, 1, 25, 3, 25, 391, 8, 25, 1, 26, 1, 26, 1, 26, 3, 26, 396, 8, 26, 1, 27, 1, 27, 1, 27, 5, 27, 401, 8, 27, 10, 27, 12, 27, 404, 9, 27, 1, 28, 1, 28, 1, 28, 1, 28, 1, 29, 3, 29, 411, 8, 29, 1, 29, 3, 29, 414, 8, 29, 1, 29, 3, 29, 417, 8, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 3, 33, 436, 8, 33, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 1, 34, 3, 34, 450, 8, 34, 1, 35, 1, 35, 1, 35, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 464, 8, 36, 10, 36, 12, 36, 467, 9, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 476, 8, 36, 10, 36, 12, 36, 479, 9, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 5, 36, 488, 8, 36, 10, 36, 12, 36, 491, 9, 36, 1, 36, 1, 36, 1, 36, 1, 36, 1, 36, 3, 36, 498, 8, 36, 1, 36, 1, 36, 3, 36, 502, 8, 36, 1, 37, 1, 37, 1, 37, 5, 37, 507, 8, 37, 10, 37, 12, 37, 510, 9, 37, 1, 38, 1, 38, 1, 38, 3, 38, 515, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 523, 8, 38, 1, 39, 1, 39, 1, 39, 3, 39, 528, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 4, 39, 535, 8, 39, 11, 39, 12, 39, 536, 1, 39, 1, 39, 3, 39, 541, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 572, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 589, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 601, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 611, 8, 39, 1, 39, 3, 39, 614, 8, 39, 1, 39, 1, 39, 3, 39, 618, 8, 39, 1, 39, 3, 39, 621, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 633, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 650, 8, 39, 1, 39, 1, 39, 3, 39, 654, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 660, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 667, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 679, 8, 39, 1, 39, 3, 39, 682, 8, 39, 1, 39, 1, 39, 3, 39, 686, 8, 39, 1, 39, 3, 39, 689, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 700, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 724, 8, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 1, 39, 3, 39, 733, 8, 39, 5, 39, 735, 8, 39, 10, 39, 12, 39, 738, 9, 39, 1, 40, 1, 40, 1, 40, 5, 40, 743, 8, 40, 10, 40, 12, 40, 746, 9, 40, 1, 41, 1, 41, 3, 41, 750, 8, 41, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 756, 8, 42, 10, 42, 12, 42, 759, 9, 42, 1, 42, 1, 42, 1, 42, 1, 42, 1, 42, 5, 42, 766, 8, 42, 10, 42, 12, 42, 769, 9, 42, 3, 42, 771, 8, 42, 1, 42, 1, 42, 1, 42, 1, 43, 1, 43, 1, 43, 1, 43, 3, 43, 780, 8, 43, 1, 43, 3, 43, 783, 8, 43, 1, 44, 1, 44, 1, 44, 5, 44, 788, 8, 44, 10, 44, 12, 44, 791, 9, 44, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 1, 45, 3, 45, 800, 8, 45, 1, 45, 1, 45, 1, 45, 1, 45, 3, 45, 806, 8, 45, 5, 45, 808, 8, 45, 10, 45, 12, 45, 811, 9, 45, 1, 46, 1, 46, 1, 46, 3, 46, 816, 8, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 3, 47, 823, 8, 47, 1, 47, 1, 47, 1, 48, 1, 48, 1, 48, 5, 48, 830, 8, 48, 10, 48, 12, 48, 833, 9, 48, 1, 49, 1, 49, 1, 49, 3, 49, 838, 8, 49, 1, 50, 1, 50, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 848, 8, 51, 3, 51, 850, 8, 51, 1, 52, 3, 52, 853, 8, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 1, 52, 3, 52, 861, 8, 52, 1, 53, 1, 53, 1, 53, 3, 53, 866, 8, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 1, 57, 1, 57, 3, 57, 876, 8, 57, 1, 58, 1, 58, 1, 58, 3, 58, 881, 8, 58, 1, 59, 1, 59, 3, 59, 885, 8, 59, 1, 60, 1, 60, 1, 60, 1, 60, 1, 60, 0, 3, 36, 78, 90, 61, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 120, 0, 18, 2, 0, 31, 31, 140, 140, 2, 0, 83, 83, 95, 95, 2, 0, 70, 70, 100, 100, 3, 0, 4, 4, 8, 8, 12, 12, 4, 0, 4, 4, 7, 8, 12, 12, 146, 146, 2, 0, 95, 95, 139, 139, 2, 0, 4, 4, 8, 8, 2, 0, 117, 117, 205, 205, 2, 0, 11, 11, 41, 42, 2, 0, 61, 61, 92, 92, 2, 0, 132, 132, 142, 142, 3, 0, 17, 17, 94, 94, 169, 169, 2, 0, 78, 78, 97, 97, 1, 0, 195, 196, 2, 0, 207, 207, 222, 222, 8, 0, 36, 36, 75, 75, 107, 107, 109, 109, 131, 131, 144, 144, 184, 184, 189, 189, 12, 0, 2, 35, 37, 74, 76, 80, 82, 106, 108, 108, 110, 111, 113, 114, 116, 129, 132, 143, 145, 183, 185, 188, 190, 191, 4, 0, 35, 35, 61, 61, 76, 76, 90, 90, 1000, 0, 124, 1, 0, 0, 0, 2, 128, 1, 0, 0, 0, 4, 142, 1, 0, 0, 0, 6, 145, 1, 0, 0, 0, 8, 196, 1, 0, 0, 0, 10, 199, 1, 0, 0, 0, 12, 205, 1, 0, 0, 0, 14, 209, 1, 0, 0, 0, 16, 215, 1, 0, 0, 0, 18, 222, 1, 0, 0, 0, 20, 225, 1, 0, 0, 0, 22, 228, 1, 0, 0, 0, 24, 238, 1, 0, 0, 0, 26, 241, 1, 0, 0, 0, 28, 245, 1, 0, 0, 0, 30, 249, 1, 0, 0, 0, 32, 254, 1, 0, 0, 0, 34, 260, 1, 0, 0, 0, 36, 275, 1, 0, 0, 0, 38, 340, 1, 0, 0, 0, 40, 348, 1, 0, 0, 0, 42, 359, 1, 0, 0, 0, 44, 361, 1, 0, 0, 0, 46, 367, 1, 0, 0, 0, 48, 372, 1, 0, 0, 0, 50, 380, 1, 0, 0, 0, 52, 392, 1, 0, 0, 0, 54, 397, 1, 0, 0, 0, 56, 405, 1, 0, 0, 0, 58, 410, 1, 0, 0, 0, 60, 418, 1, 0, 0, 0, 62, 422, 1, 0, 0, 0, 64, 426, 1, 0, 0, 0, 66, 435, 1, 0, 0, 0, 68, 449, 1, 0, 0, 0, 70, 451, 1, 0, 0, 0, 72, 501, 1, 0, 0, 0, 74, 503, 1, 0, 0, 0, 76, 522, 1, 0, 0, 0, 78, 653, 1, 0, 0, 0, 80, 739, 1, 0, 0, 0, 82, 749, 1, 0, 0, 0, 84, 770, 1, 0, 0, 0, 86, 782, 1, 0, 0, 0, 88, 784, 1, 0, 0, 0, 90, 799, 1, 0, 0, 0, 92, 812, 1, 0, 0, 0, 94, 822, 1, 0, 0, 0, 96, 826, 1, 0, 0, 0, 98, 837, 1, 0, 0, 0, 100, 839, 1, 0, 0, 0, 102, 849, 1, 0, 0, 0, 104, 852, 1, 0, 0, 0, 106, 865, 1, 0, 0, 0, 108, 867, 1, 0, 0, 0, 110, 869, 1, 0, 0, 0, 112, 871, 1, 0, 0, 0, 114, 875, 1, 0, 0, 0, 116, 880, 1, 0, 0, 0, 118, 884, 1, 0, 0, 0, 120, 886, 1, 0, 0, 0, 122, 125, 3, 2, 1, 0, 123, 125, 3, 6, 3, 0, 124, 122, 1, 0, 0, 0, 124, 123, 1, 0, 0, 0, 125, 126, 1, 0, 0, 0, 126, 127, 5, 0, 0, 1, 127, 1, 1, 0, 0, 0, 128, 134, 3, 4, 2, 0, 129, 130, 5, 175, 0, 0, 130, 131, 5, 4, 0, 0, 131, 133, 3, 4, 2, 0, 132, 129, 1, 0, 0, 0, 133, 136, 1, 0, 0, 0, 134, 132, 1, 0, 0, 0, 134, 135, 1, 0, 0, 0, 135, 3, 1, 0, 0, 0, 136, 134, 1, 0, 0, 0, 137, 143, 3, 6, 3, 0, 138, 139, 5, 218, 0, 0, 139, 140, 3, 2, 1, 0, 140, 141, 5, 228, 0, 0, 141, 143, 1, 0, 0, 0, 142, 137, 1, 0, 0, 0, 142, 138, 1, 0, 0, 0, 143, 5, 1, 0, 0, 0, 144, 146, 3, 8, 4, 0, 145, 144, 1, 0, 0, 0, 145, 146, 1, 0, 0, 0, 146, 147, 1, 0, 0, 0, 147, 149, 5, 145, 0, 0, 148, 150, 5, 48, 0, 0, 149, 148, 1, 0, 0, 0, 149, 150, 1, 0, 0, 0, 150, 152, 1, 0, 0, 0, 151, 153, 3, 10, 5, 0, 152, 151, 1, 0, 0, 0, 152, 153, 1, 0, 0, 0, 153, 154, 1, 0, 0, 0, 154, 156, 3, 74, 37, 0, 155, 157, 3, 12, 6, 0, 156, 155, 1, 0, 0, 0, 156, 157, 1, 0, 0, 0, 157, 159, 1, 0, 0, 0, 158, 160, 3, 14, 7, 0, 159, 158, 1, 0, 0, 0, 159, 160, 1, 0, 0, 0, 160, 162, 1, 0, 0, 0, 161, 163, 3, 16, 8, 0, 162, 161, 1, 0, 0, 0, 162, 163, 1, 0, 0, 0, 163, 165, 1, 0, 0, 0, 164, 166, 3, 18, 9, 0, 165, 164, 1, 0, 0, 0, 165, 166, 1, 0, 0, 0, 166, 168, 1, 0, 0, 0, 167, 169, 3, 20, 10, 0, 168, 167, 1, 0, 0, 0, 168, 169, 1, 0, 0, 0, 169, 171, 1, 0, 0, 0, 170, 172, 3, 22, 11, 0, 171, 170, 1, 0, 0, 0, 171, 172, 1, 0, 0, 0, 172, 175, 1, 0, 0, 0, 173, 174, 5, 188, 0, 0, 174, 176, 7, 0, 0, 0, 175, 173, 1, 0, 0, 0, 175, 176, 1, 0, 0, 0, 176, 179, 1, 0, 0, 0, 177, 178, 5, 188, 0, 0, 178, 180, 5, 168, 0, 0, 179, 177, 1, 0, 0, 0, 179, 180, 1, 0, 0, 0, 180, 182, 1, 0, 0, 0, 181, 183, 3, 24, 12, 0, 182, 181, 1, 0, 0, 0, 182, 183, 1, 0, 0, 0, 183, 185, 1, 0, 0, 0, 184, 186, 3, 26, 13, 0, 185, 184, 1, 0, 0, 0, 185, 186, 1, 0, 0, 0, 186, 188, 1, 0, 0, 0, 187, 189, 3, 30, 15, 0, 188, 187, 1, 0, 0, 0, 188, 189, 1, 0, 0, 0, 189, 191, 1, 0, 0, 0, 190, 192, 3, 32, 16, 0, 191, 190, 1, 0, 0, 0, 191, 192, 1, 0, 0, 0, 192, 194, 1, 0, 0, 0, 193, 195, 3, 34, 17, 0, 194, 193, 1, 0, 0, 0, 194, 195, 1, 0, 0, 0, 195, 7, 1, 0, 0, 0, 196, 197, 5, 188, 0, 0, 197, 198, 3, 74, 37, 0, 198, 9, 1, 0, 0, 0, 199, 200, 5, 167, 0, 0, 200, 203, 5, 196, 0, 0, 201, 202, 5, 188, 0, 0, 202, 204, 5, 163, 0, 0, 203, 201, 1, 0, 0, 0, 203, 204, 1, 0, 0, 0, 204, 11, 1, 0, 0, 0, 205, 206, 5, 67, 0, 0, 206, 207, 3, 36, 18, 0, 207, 13, 1, 0, 0, 0, 208, 210, 7, 1, 0, 0, 209, 208, 1, 0, 0, 0, 209, 210, 1, 0, 0, 0, 210, 211, 1, 0, 0, 0, 211, 212, 5, 9, 0, 0, 212, 213, 5, 89, 0, 0, 213, 214, 3, 74, 37, 0, 214, 15, 1, 0, 0, 0, 215, 216, 5, 187, 0, 0, 216, 217, 3, 116, 58, 0, 217, 218, 5, 10, 0, 0, 218, 219, 5, 218, 0, 0, 219, 220, 3, 58, 29, 0, 220, 221, 5, 228, 0, 0, 221, 17, 1, 0, 0, 0, 222, 223, 5, 128, 0, 0, 223, 224, 3, 78, 39, 0, 224, 19, 1, 0, 0, 0, 225, 226, 5, 186, 0, 0, 226, 227, 3, 78, 39, 0, 227, 21, 1, 0, 0, 0, 228, 229, 5, 72, 0, 0, 229, 236, 5, 18, 0, 0, 230, 231, 7, 0, 0, 0, 231, 232, 5, 218, 0, 0, 232, 233, 3, 74, 37, 0, 233, 234, 5, 228, 0, 0, 234, 237, 1, 0, 0, 0, 235, 237, 3, 74, 37, 0, 236, 230, 1, 0, 0, 0, 236, 235, 1, 0, 0, 0, 237, 23, 1, 0, 0, 0, 238, 239, 5, 73, 0, 0, 239, 240, 3, 78, 39, 0, 240, 25, 1, 0, 0, 0, 241, 242, 5, 121, 0, 0, 242, 243, 5, 18, 0, 0, 243, 244, 3, 48, 24, 0, 244, 27, 1, 0, 0, 0, 245, 246, 5, 121, 0, 0, 246, 247, 5, 18, 0, 0, 247, 248, 3, 74, 37, 0, 248, 29, 1, 0, 0, 0, 249, 250, 5, 98, 0, 0, 250, 251, 3, 46, 23, 0, 251, 252, 5, 18, 0, 0, 252, 253, 3, 74, 37, 0, 253, 31, 1, 0, 0, 0, 254, 255, 5, 98, 0, 0, 255, 258, 3, 46, 23, 0, 256, 257, 5, 188, 0, 0, 257, 259, 5, 163, 0, 0, 258, 256, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 33, 1, 0, 0, 0, 260, 261, 5, 149, 0, 0, 261, 262, 3, 54, 27, 0, 262, 35, 1, 0, 0, 0, 263, 264, 6, 18, -1, 0, 264, 266, 3, 90, 45, 0, 265, 267, 5, 60, 0, 0, 266, 265, 1, 0, 0, 0, 266, 267, 1, 0, 0, 0, 267, 269, 1, 0, 0, 0, 268, 270, 3, 44, 22, 0, 269, 268, 1, 0, 0, 0, 269, 270, 1, 0, 0, 0, 270, 276, 1, 0, 0, 0, 271, 272, 5, 218, 0, 0, 272, 273, 3, 36, 18, 0, 273, 274, 5, 228, 0, 0, 274, 276, 1, 0, 0, 0, 275, 263, 1, 0, 0, 0, 275, 271, 1, 0, 0, 0, 276, 294, 1, 0, 0, 0, 277, 278, 10, 3, 0, 0, 278, 279, 3, 40, 20, 0, 279, 280, 3, 36, 18, 4, 280, 293, 1, 0, 0, 0, 281, 283, 10, 4, 0, 0, 282, 284, 7, 2, 0, 0, 283, 282, 1, 0, 0, 0, 283, 284, 1, 0, 0, 0, 284, 286, 1, 0, 0, 0, 285, 287, 3, 38, 19, 0, 286, 285, 1, 0, 0, 0, 286, 287, 1, 0, 0, 0, 287, 288, 1, 0, 0, 0, 288, 289, 5, 89, 0, 0, 289, 290, 3, 36, 18, 0, 290, 291, 3, 42, 21, 0, 291, 293, 1, 0, 0, 0, 292, 277, 1, 0, 0, 0, 292, 281, 1, 0, 0, 0, 293, 296, 1, 0, 0, 0, 294, 292, 1, 0, 0, 0, 294, 295, 1, 0, 0, 0, 295, 37, 1, 0, 0, 0, 296, 294, 1, 0, 0, 0, 297, 299, 7, 3, 0, 0, 298, 297, 1, 0, 0, 0, 298, 299, 1, 0, 0, 0, 299, 300, 1, 0, 0, 0, 300, 307, 5, 83, 0, 0, 301, 303, 5, 83, 0, 0, 302, 304, 7, 3, 0, 0, 303, 302, 1, 0, 0, 0, 303, 304, 1, 0, 0, 0, 304, 307, 1, 0, 0, 0, 305, 307, 7, 3, 0, 0, 306, 298, 1, 0, 0, 0, 306, 301, 1, 0, 0, 0, 306, 305, 1, 0, 0, 0, 307, 341, 1, 0, 0, 0, 308, 310, 7, 4, 0, 0, 309, 308, 1, 0, 0, 0, 309, 310, 1, 0, 0, 0, 310, 311, 1, 0, 0, 0, 311, 313, 7, 5, 0, 0, 312, 314, 5, 122, 0, 0, 313, 312, 1, 0, 0, 0, 313, 314, 1, 0, 0, 0, 314, 323, 1, 0, 0, 0, 315, 317, 7, 5, 0, 0, 316, 318, 5, 122, 0, 0, 317, 316, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 320, 1, 0, 0, 0, 319, 321, 7, 4, 0, 0, 320, 319, 1, 0, 0, 0, 320, 321, 1, 0, 0, 0, 321, 323, 1, 0, 0, 0, 322, 309, 1, 0, 0, 0, 322, 315, 1, 0, 0, 0, 323, 341, 1, 0, 0, 0, 324, 326, 7, 6, 0, 0, 325, 324, 1, 0, 0, 0, 325, 326, 1, 0, 0, 0, 326, 327, 1, 0, 0, 0, 327, 329, 5, 68, 0, 0, 328, 330, 5, 122, 0, 0, 329, 328, 1, 0, 0, 0, 329, 330, 1, 0, 0, 0, 330, 339, 1, 0, 0, 0, 331, 333, 5, 68, 0, 0, 332, 334, 5, 122, 0, 0, 333, 332, 1, 0, 0, 0, 333, 334, 1, 0, 0, 0, 334, 336, 1, 0, 0, 0, 335, 337, 7, 6, 0, 0, 336, 335, 1, 0, 0, 0, 336, 337, 1, 0, 0, 0, 337, 339, 1, 0, 0, 0, 338, 325, 1, 0, 0, 0, 338, 331, 1, 0, 0, 0, 339, 341, 1, 0, 0, 0, 340, 306, 1, 0, 0, 0, 340, 322, 1, 0, 0, 0, 340, 338, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 344, 7, 2, 0, 0, 343, 342, 1, 0, 0, 0, 343, 344, 1, 0, 0, 0, 344, 345, 1, 0, 0, 0, 345, 346, 5, 30, 0, 0, 346, 349, 5, 89, 0, 0, 347, 349, 5, 205, 0, 0, 348, 343, 1, 0, 0, 0, 348, 347, 1, 0, 0, 0, 349, 41, 1, 0, 0, 0, 350, 351, 5, 118, 0, 0, 351, 360, 3, 74, 37, 0, 352, 353, 5, 178, 0, 0, 353, 354, 5, 218, 0, 0, 354, 355, 3, 74, 37, 0, 355, 356, 5, 228, 0, 0, 356, 360, 1, 0, 0, 0, 357, 358, 5, 178, 0, 0, 358, 360, 3, 74, 37, 0, 359, 350, 1, 0, 0, 0, 359, 352, 1, 0, 0, 0, 359, 357, 1, 0, 0, 0, 360, 43, 1, 0, 0, 0, 361, 362, 5, 143, 0, 0, 362, 365, 3, 52, 26, 0, 363, 364, 5, 117, 0, 0, 364, 366, 3, 52, 26, 0, 365, 363, 1, 0, 0, 0, 365, 366, 1, 0, 0, 0, 366, 45, 1, 0, 0, 0, 367, 370, 3, 78, 39, 0, 368, 369, 7, 7, 0, 0, 369, 371, 3, 78, 39, 0, 370, 368, 1, 0, 0, 0, 370, 371, 1, 0, 0, 0, 371, 47, 1, 0, 0, 0, 372, 377, 3, 50, 25, 0, 373, 374, 5, 205, 0, 0, 374, 376, 3, 50, 25, 0, 375, 373, 1, 0, 0, 0, 376, 379, 1, 0, 0, 0, 377, 375, 1, 0, 0, 0, 377, 378, 1, 0, 0, 0, 378, 49, 1, 0, 0, 0, 379, 377, 1, 0, 0, 0, 380, 382, 3, 78, 39, 0, 381, 383, 7, 8, 0, 0, 382, 381, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 386, 1, 0, 0, 0, 384, 385, 5, 116, 0, 0, 385, 387, 7, 9, 0, 0, 386, 384, 1, 0, 0, 0, 386, 387, 1, 0, 0, 0, 387, 390, 1, 0, 0, 0, 388, 389, 5, 25, 0, 0, 389, 391, 5, 198, 0, 0, 390, 388, 1, 0, 0, 0, 390, 391, 1, 0, 0, 0, 391, 51, 1, 0, 0, 0, 392, 395, 3, 104, 52, 0, 393, 394, 5, 230, 0, 0, 394, 396, 3, 104, 52, 0, 395, 393, 1, 0, 0, 0, 395, 396, 1, 0, 0, 0, 396, 53, 1, 0, 0, 0, 397, 402, 3, 56, 28, 0, 398, 399, 5, 205, 0, 0, 399, 401, 3, 56, 28, 0, 400, 398, 1, 0, 0, 0, 401, 404, 1, 0, 0, 0, 402, 400, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 55, 1, 0, 0, 0, 404, 402, 1, 0, 0, 0, 405, 406, 3, 116, 58, 0, 406, 407, 5, 211, 0, 0, 407, 408, 3, 106, 53, 0, 408, 57, 1, 0, 0, 0, 409, 411, 3, 60, 30, 0, 410, 409, 1, 0, 0, 0, 410, 411, 1, 0, 0, 0, 411, 413, 1, 0, 0, 0, 412, 414, 3, 62, 31, 0, 413, 412, 1, 0, 0, 0, 413, 414, 1, 0, 0, 0, 414, 416, 1, 0, 0, 0, 415, 417, 3, 64, 32, 0, 416, 415, 1, 0, 0, 0, 416, 417, 1, 0, 0, 0, 417, 59, 1, 0, 0, 0, 418, 419, 5, 125, 0, 0, 419, 420, 5, 18, 0, 0, 420, 421, 3, 74, 37, 0, 421, 61, 1, 0, 0, 0, 422, 423, 5, 121, 0, 0, 423, 424, 5, 18, 0, 0, 424, 425, 3, 48, 24, 0, 425, 63, 1, 0, 0, 0, 426, 427, 7, 10, 0, 0, 427, 428, 3, 66, 33, 0, 428, 65, 1, 0, 0, 0, 429, 436, 3, 68, 34, 0, 430, 431, 5, 16, 0, 0, 431, 432, 3, 68, 34, 0, 432, 433, 5, 6, 0, 0, 433, 434, 3, 68, 34, 0, 434, 436, 1, 0, 0, 0, 435, 429, 1, 0, 0, 0, 435, 430, 1, 0, 0, 0, 436, 67, 1, 0, 0, 0, 437, 438, 5, 32, 0, 0, 438, 450, 5, 141, 0, 0, 439, 440, 5, 174, 0, 0, 440, 450, 5, 127, 0, 0, 441, 442, 5, 174, 0, 0, 442, 450, 5, 63, 0, 0, 443, 444, 3, 104, 52, 0, 444, 445, 5, 127, 0, 0, 445, 450, 1, 0, 0, 0, 446, 447, 3, 104, 52, 0, 447, 448, 5, 63, 0, 0, 448, 450, 1, 0, 0, 0, 449, 437, 1, 0, 0, 0, 449, 439, 1, 0, 0, 0, 449, 441, 1, 0, 0, 0, 449, 443, 1, 0, 0, 0, 449, 446, 1, 0, 0, 0, 450, 69, 1, 0, 0, 0, 451, 452, 3, 78, 39, 0, 452, 453, 5, 0, 0, 1, 453, 71, 1, 0, 0, 0, 454, 502, 3, 116, 58, 0, 455, 456, 3, 116, 58, 0, 456, 457, 5, 218, 0, 0, 457, 458, 3, 116, 58, 0, 458, 465, 3, 72, 36, 0, 459, 460, 5, 205, 0, 0, 460, 461, 3, 116, 58, 0, 461, 462, 3, 72, 36, 0, 462, 464, 1, 0, 0, 0, 463, 459, 1, 0, 0, 0, 464, 467, 1, 0, 0, 0, 465, 463, 1, 0, 0, 0, 465, 466, 1, 0, 0, 0, 466, 468, 1, 0, 0, 0, 467, 465, 1, 0, 0, 0, 468, 469, 5, 228, 0, 0, 469, 502, 1, 0, 0, 0, 470, 471, 3, 116, 58, 0, 471, 472, 5, 218, 0, 0, 472, 477, 3, 120, 60, 0, 473, 474, 5, 205, 0, 0, 474, 476, 3, 120, 60, 0, 475, 473, 1, 0, 0, 0, 476, 479, 1, 0, 0, 0, 477, 475, 1, 0, 0, 0, 477, 478, 1, 0, 0, 0, 478, 480, 1, 0, 0, 0, 479, 477, 1, 0, 0, 0, 480, 481, 5, 228, 0, 0, 481, 502, 1, 0, 0, 0, 482, 483, 3, 116, 58, 0, 483, 484, 5, 218, 0, 0, 484, 489, 3, 72, 36, 0, 485, 486, 5, 205, 0, 0, 486, 488, 3, 72, 36, 0, 487, 485, 1, 0, 0, 0, 488, 491, 1, 0, 0, 0, 489, 487, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 492, 1, 0, 0, 0, 491, 489, 1, 0, 0, 0, 492, 493, 5, 228, 0, 0, 493, 502, 1, 0, 0, 0, 494, 495, 3, 116, 58, 0, 495, 497, 5, 218, 0, 0, 496, 498, 3, 74, 37, 0, 497, 496, 1, 0, 0, 0, 497, 498, 1, 0, 0, 0, 498, 499, 1, 0, 0, 0, 499, 500, 5, 228, 0, 0, 500, 502, 1, 0, 0, 0, 501, 454, 1, 0, 0, 0, 501, 455, 1, 0, 0, 0, 501, 470, 1, 0, 0, 0, 501, 482, 1, 0, 0, 0, 501, 494, 1, 0, 0, 0, 502, 73, 1, 0, 0, 0, 503, 508, 3, 76, 38, 0, 504, 505, 5, 205, 0, 0, 505, 507, 3, 76, 38, 0, 506, 504, 1, 0, 0, 0, 507, 510, 1, 0, 0, 0, 508, 506, 1, 0, 0, 0, 508, 509, 1, 0, 0, 0, 509, 75, 1, 0, 0, 0, 510, 508, 1, 0, 0, 0, 511, 512, 3, 94, 47, 0, 512, 513, 5, 209, 0, 0, 513, 515, 1, 0, 0, 0, 514, 511, 1, 0, 0, 0, 514, 515, 1, 0, 0, 0, 515, 516, 1, 0, 0, 0, 516, 523, 5, 201, 0, 0, 517, 518, 5, 218, 0, 0, 518, 519, 3, 2, 1, 0, 519, 520, 5, 228, 0, 0, 520, 523, 1, 0, 0, 0, 521, 523, 3, 78, 39, 0, 522, 514, 1, 0, 0, 0, 522, 517, 1, 0, 0, 0, 522, 521, 1, 0, 0, 0, 523, 77, 1, 0, 0, 0, 524, 525, 6, 39, -1, 0, 525, 527, 5, 19, 0, 0, 526, 528, 3, 78, 39, 0, 527, 526, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 534, 1, 0, 0, 0, 529, 530, 5, 185, 0, 0, 530, 531, 3, 78, 39, 0, 531, 532, 5, 162, 0, 0, 532, 533, 3, 78, 39, 0, 533, 535, 1, 0, 0, 0, 534, 529, 1, 0, 0, 0, 535, 536, 1, 0, 0, 0, 536, 534, 1, 0, 0, 0, 536, 537, 1, 0, 0, 0, 537, 540, 1, 0, 0, 0, 538, 539, 5, 51, 0, 0, 539, 541, 3, 78, 39, 0, 540, 538, 1, 0, 0, 0, 540, 541, 1, 0, 0, 0, 541, 542, 1, 0, 0, 0, 542, 543, 5, 52, 0, 0, 543, 654, 1, 0, 0, 0, 544, 545, 5, 20, 0, 0, 545, 546, 5, 218, 0, 0, 546, 547, 3, 78, 39, 0, 547, 548, 5, 10, 0, 0, 548, 549, 3, 72, 36, 0, 549, 550, 5, 228, 0, 0, 550, 654, 1, 0, 0, 0, 551, 552, 5, 35, 0, 0, 552, 654, 5, 198, 0, 0, 553, 554, 5, 58, 0, 0, 554, 555, 5, 218, 0, 0, 555, 556, 3, 108, 54, 0, 556, 557, 5, 67, 0, 0, 557, 558, 3, 78, 39, 0, 558, 559, 5, 228, 0, 0, 559, 654, 1, 0, 0, 0, 560, 561, 5, 85, 0, 0, 561, 562, 3, 78, 39, 0, 562, 563, 3, 108, 54, 0, 563, 654, 1, 0, 0, 0, 564, 565, 5, 154, 0, 0, 565, 566, 5, 218, 0, 0, 566, 567, 3, 78, 39, 0, 567, 568, 5, 67, 0, 0, 568, 571, 3, 78, 39, 0, 569, 570, 5, 64, 0, 0, 570, 572, 3, 78, 39, 0, 571, 569, 1, 0, 0, 0, 571, 572, 1, 0, 0, 0, 572, 573, 1, 0, 0, 0, 573, 574, 5, 228, 0, 0, 574, 654, 1, 0, 0, 0, 575, 576, 5, 165, 0, 0, 576, 654, 5, 198, 0, 0, 577, 578, 5, 170, 0, 0, 578, 579, 5, 218, 0, 0, 579, 580, 7, 11, 0, 0, 580, 581, 5, 198, 0, 0, 581, 582, 5, 67, 0, 0, 582, 583, 3, 78, 39, 0, 583, 584, 5, 228, 0, 0, 584, 654, 1, 0, 0, 0, 585, 586, 3, 116, 58, 0, 586, 588, 5, 218, 0, 0, 587, 589, 3, 74, 37, 0, 588, 587, 1, 0, 0, 0, 588, 589, 1, 0, 0, 0, 589, 590, 1, 0, 0, 0, 590, 591, 5, 228, 0, 0, 591, 592, 1, 0, 0, 0, 592, 593, 5, 124, 0, 0, 593, 594, 5, 218, 0, 0, 594, 595, 3, 58, 29, 0, 595, 596, 5, 228, 0, 0, 596, 654, 1, 0, 0, 0, 597, 598, 3, 116, 58, 0, 598, 600, 5, 218, 0, 0, 599, 601, 3, 74, 37, 0, 600, 599, 1, 0, 0, 0, 600, 601, 1, 0, 0, 0, 601, 602, 1, 0, 0, 0, 602, 603, 5, 228, 0, 0, 603, 604, 1, 0, 0, 0, 604, 605, 5, 124, 0, 0, 605, 606, 3, 116, 58, 0, 606, 654, 1, 0, 0, 0, 607, 613, 3, 116, 58, 0, 608, 610, 5, 218, 0, 0, 609, 611, 3, 74, 37, 0, 610, 609, 1, 0, 0, 0, 610, 611, 1, 0, 0, 0, 611, 612, 1, 0, 0, 0, 612, 614, 5, 228, 0, 0, 613, 608, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 615, 1, 0, 0, 0, 615, 617, 5, 218, 0, 0, 616, 618, 5, 48, 0, 0, 617, 616, 1, 0, 0, 0, 617, 618, 1, 0, 0, 0, 618, 620, 1, 0, 0, 0, 619, 621, 3, 80, 40, 0, 620, 619, 1, 0, 0, 0, 620, 621, 1, 0, 0, 0, 621, 622, 1, 0, 0, 0, 622, 623, 5, 228, 0, 0, 623, 654, 1, 0, 0, 0, 624, 654, 3, 106, 53, 0, 625, 626, 5, 207, 0, 0, 626, 654, 3, 78, 39, 17, 627, 628, 5, 114, 0, 0, 628, 654, 3, 78, 39, 12, 629, 630, 3, 94, 47, 0, 630, 631, 5, 209, 0, 0, 631, 633, 1, 0, 0, 0, 632, 629, 1, 0, 0, 0, 632, 633, 1, 0, 0, 0, 633, 634, 1, 0, 0, 0, 634, 654, 5, 201, 0, 0, 635, 636, 5, 218, 0, 0, 636, 637, 3, 2, 1, 0, 637, 638, 5, 228, 0, 0, 638, 654, 1, 0, 0, 0, 639, 640, 5, 218, 0, 0, 640, 641, 3, 78, 39, 0, 641, 642, 5, 228, 0, 0, 642, 654, 1, 0, 0, 0, 643, 644, 5, 218, 0, 0, 644, 645, 3, 74, 37, 0, 645, 646, 5, 228, 0, 0, 646, 654, 1, 0, 0, 0, 647, 649, 5, 216, 0, 0, 648, 650, 3, 74, 37, 0, 649, 648, 1, 0, 0, 0, 649, 650, 1, 0, 0, 0, 650, 651, 1, 0, 0, 0, 651, 654, 5, 227, 0, 0, 652, 654, 3, 86, 43, 0, 653, 524, 1, 0, 0, 0, 653, 544, 1, 0, 0, 0, 653, 551, 1, 0, 0, 0, 653, 553, 1, 0, 0, 0, 653, 560, 1, 0, 0, 0, 653, 564, 1, 0, 0, 0, 653, 575, 1, 0, 0, 0, 653, 577, 1, 0, 0, 0, 653, 585, 1, 0, 0, 0, 653, 597, 1, 0, 0, 0, 653, 607, 1, 0, 0, 0, 653, 624, 1, 0, 0, 0, 653, 625, 1, 0, 0, 0, 653, 627, 1, 0, 0, 0, 653, 632, 1, 0, 0, 0, 653, 635, 1, 0, 0, 0, 653, 639, 1, 0, 0, 0, 653, 643, 1, 0, 0, 0, 653, 647, 1, 0, 0, 0, 653, 652, 1, 0, 0, 0, 654, 736, 1, 0, 0, 0, 655, 659, 10, 16, 0, 0, 656, 660, 5, 201, 0, 0, 657, 660, 5, 230, 0, 0, 658, 660, 5, 221, 0, 0, 659, 656, 1, 0, 0, 0, 659, 657, 1, 0, 0, 0, 659, 658, 1, 0, 0, 0, 660, 661, 1, 0, 0, 0, 661, 735, 3, 78, 39, 17, 662, 666, 10, 15, 0, 0, 663, 667, 5, 222, 0, 0, 664, 667, 5, 207, 0, 0, 665, 667, 5, 206, 0, 0, 666, 663, 1, 0, 0, 0, 666, 664, 1, 0, 0, 0, 666, 665, 1, 0, 0, 0, 667, 668, 1, 0, 0, 0, 668, 735, 3, 78, 39, 16, 669, 688, 10, 14, 0, 0, 670, 689, 5, 210, 0, 0, 671, 689, 5, 211, 0, 0, 672, 689, 5, 220, 0, 0, 673, 689, 5, 217, 0, 0, 674, 689, 5, 212, 0, 0, 675, 689, 5, 219, 0, 0, 676, 689, 5, 213, 0, 0, 677, 679, 5, 70, 0, 0, 678, 677, 1, 0, 0, 0, 678, 679, 1, 0, 0, 0, 679, 681, 1, 0, 0, 0, 680, 682, 5, 114, 0, 0, 681, 680, 1, 0, 0, 0, 681, 682, 1, 0, 0, 0, 682, 683, 1, 0, 0, 0, 683, 689, 5, 79, 0, 0, 684, 686, 5, 114, 0, 0, 685, 684, 1, 0, 0, 0, 685, 686, 1, 0, 0, 0, 686, 687, 1, 0, 0, 0, 687, 689, 7, 12, 0, 0, 688, 670, 1, 0, 0, 0, 688, 671, 1, 0, 0, 0, 688, 672, 1, 0, 0, 0, 688, 673, 1, 0, 0, 0, 688, 674, 1, 0, 0, 0, 688, 675, 1, 0, 0, 0, 688, 676, 1, 0, 0, 0, 688, 678, 1, 0, 0, 0, 688, 685, 1, 0, 0, 0, 689, 690, 1, 0, 0, 0, 690, 735, 3, 78, 39, 15, 691, 692, 10, 11, 0, 0, 692, 693, 5, 6, 0, 0, 693, 735, 3, 78, 39, 12, 694, 695, 10, 10, 0, 0, 695, 696, 5, 120, 0, 0, 696, 735, 3, 78, 39, 11, 697, 699, 10, 9, 0, 0, 698, 700, 5, 114, 0, 0, 699, 698, 1, 0, 0, 0, 699, 700, 1, 0, 0, 0, 700, 701, 1, 0, 0, 0, 701, 702, 5, 16, 0, 0, 702, 703, 3, 78, 39, 0, 703, 704, 5, 6, 0, 0, 704, 705, 3, 78, 39, 10, 705, 735, 1, 0, 0, 0, 706, 707, 10, 8, 0, 0, 707, 708, 5, 223, 0, 0, 708, 709, 3, 78, 39, 0, 709, 710, 5, 204, 0, 0, 710, 711, 3, 78, 39, 8, 711, 735, 1, 0, 0, 0, 712, 713, 10, 19, 0, 0, 713, 714, 5, 216, 0, 0, 714, 715, 3, 78, 39, 0, 715, 716, 5, 227, 0, 0, 716, 735, 1, 0, 0, 0, 717, 718, 10, 18, 0, 0, 718, 719, 5, 209, 0, 0, 719, 735, 5, 196, 0, 0, 720, 721, 10, 13, 0, 0, 721, 723, 5, 87, 0, 0, 722, 724, 5, 114, 0, 0, 723, 722, 1, 0, 0, 0, 723, 724, 1, 0, 0, 0, 724, 725, 1, 0, 0, 0, 725, 735, 5, 115, 0, 0, 726, 732, 10, 7, 0, 0, 727, 733, 3, 114, 57, 0, 728, 729, 5, 10, 0, 0, 729, 733, 3, 116, 58, 0, 730, 731, 5, 10, 0, 0, 731, 733, 5, 198, 0, 0, 732, 727, 1, 0, 0, 0, 732, 728, 1, 0, 0, 0, 732, 730, 1, 0, 0, 0, 733, 735, 1, 0, 0, 0, 734, 655, 1, 0, 0, 0, 734, 662, 1, 0, 0, 0, 734, 669, 1, 0, 0, 0, 734, 691, 1, 0, 0, 0, 734, 694, 1, 0, 0, 0, 734, 697, 1, 0, 0, 0, 734, 706, 1, 0, 0, 0, 734, 712, 1, 0, 0, 0, 734, 717, 1, 0, 0, 0, 734, 720, 1, 0, 0, 0, 734, 726, 1, 0, 0, 0, 735, 738, 1, 0, 0, 0, 736, 734, 1, 0, 0, 0, 736, 737, 1, 0, 0, 0, 737, 79, 1, 0, 0, 0, 738, 736, 1, 0, 0, 0, 739, 744, 3, 82, 41, 0, 740, 741, 5, 205, 0, 0, 741, 743, 3, 82, 41, 0, 742, 740, 1, 0, 0, 0, 743, 746, 1, 0, 0, 0, 744, 742, 1, 0, 0, 0, 744, 745, 1, 0, 0, 0, 745, 81, 1, 0, 0, 0, 746, 744, 1, 0, 0, 0, 747, 750, 3, 84, 42, 0, 748, 750, 3, 78, 39, 0, 749, 747, 1, 0, 0, 0, 749, 748, 1, 0, 0, 0, 750, 83, 1, 0, 0, 0, 751, 752, 5, 218, 0, 0, 752, 757, 3, 116, 58, 0, 753, 754, 5, 205, 0, 0, 754, 756, 3, 116, 58, 0, 755, 753, 1, 0, 0, 0, 756, 759, 1, 0, 0, 0, 757, 755, 1, 0, 0, 0, 757, 758, 1, 0, 0, 0, 758, 760, 1, 0, 0, 0, 759, 757, 1, 0, 0, 0, 760, 761, 5, 228, 0, 0, 761, 771, 1, 0, 0, 0, 762, 767, 3, 116, 58, 0, 763, 764, 5, 205, 0, 0, 764, 766, 3, 116, 58, 0, 765, 763, 1, 0, 0, 0, 766, 769, 1, 0, 0, 0, 767, 765, 1, 0, 0, 0, 767, 768, 1, 0, 0, 0, 768, 771, 1, 0, 0, 0, 769, 767, 1, 0, 0, 0, 770, 751, 1, 0, 0, 0, 770, 762, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 5, 200, 0, 0, 773, 774, 3, 78, 39, 0, 774, 85, 1, 0, 0, 0, 775, 783, 5, 199, 0, 0, 776, 777, 3, 94, 47, 0, 777, 778, 5, 209, 0, 0, 778, 780, 1, 0, 0, 0, 779, 776, 1, 0, 0, 0, 779, 780, 1, 0, 0, 0, 780, 781, 1, 0, 0, 0, 781, 783, 3, 88, 44, 0, 782, 775, 1, 0, 0, 0, 782, 779, 1, 0, 0, 0, 783, 87, 1, 0, 0, 0, 784, 789, 3, 116, 58, 0, 785, 786, 5, 209, 0, 0, 786, 788, 3, 116, 58, 0, 787, 785, 1, 0, 0, 0, 788, 791, 1, 0, 0, 0, 789, 787, 1, 0, 0, 0, 789, 790, 1, 0, 0, 0, 790, 89, 1, 0, 0, 0, 791, 789, 1, 0, 0, 0, 792, 793, 6, 45, -1, 0, 793, 800, 3, 94, 47, 0, 794, 800, 3, 92, 46, 0, 795, 796, 5, 218, 0, 0, 796, 797, 3, 2, 1, 0, 797, 798, 5, 228, 0, 0, 798, 800, 1, 0, 0, 0, 799, 792, 1, 0, 0, 0, 799, 794, 1, 0, 0, 0, 799, 795, 1, 0, 0, 0, 800, 809, 1, 0, 0, 0, 801, 805, 10, 1, 0, 0, 802, 806, 3, 114, 57, 0, 803, 804, 5, 10, 0, 0, 804, 806, 3, 116, 58, 0, 805, 802, 1, 0, 0, 0, 805, 803, 1, 0, 0, 0, 806, 808, 1, 0, 0, 0, 807, 801, 1, 0, 0, 0, 808, 811, 1, 0, 0, 0, 809, 807, 1, 0, 0, 0, 809, 810, 1, 0, 0, 0, 810, 91, 1, 0, 0, 0, 811, 809, 1, 0, 0, 0, 812, 813, 3, 116, 58, 0, 813, 815, 5, 218, 0, 0, 814, 816, 3, 96, 48, 0, 815, 814, 1, 0, 0, 0, 815, 816, 1, 0, 0, 0, 816, 817, 1, 0, 0, 0, 817, 818, 5, 228, 0, 0, 818, 93, 1, 0, 0, 0, 819, 820, 3, 100, 50, 0, 820, 821, 5, 209, 0, 0, 821, 823, 1, 0, 0, 0, 822, 819, 1, 0, 0, 0, 822, 823, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 825, 3, 116, 58, 0, 825, 95, 1, 0, 0, 0, 826, 831, 3, 98, 49, 0, 827, 828, 5, 205, 0, 0, 828, 830, 3, 98, 49, 0, 829, 827, 1, 0, 0, 0, 830, 833, 1, 0, 0, 0, 831, 829, 1, 0, 0, 0, 831, 832, 1, 0, 0, 0, 832, 97, 1, 0, 0, 0, 833, 831, 1, 0, 0, 0, 834, 838, 3, 88, 44, 0, 835, 838, 3, 92, 46, 0, 836, 838, 3, 106, 53, 0, 837, 834, 1, 0, 0, 0, 837, 835, 1, 0, 0, 0, 837, 836, 1, 0, 0, 0, 838, 99, 1, 0, 0, 0, 839, 840, 3, 116, 58, 0, 840, 101, 1, 0, 0, 0, 841, 850, 5, 194, 0, 0, 842, 843, 5, 209, 0, 0, 843, 850, 7, 13, 0, 0, 844, 845, 5, 196, 0, 0, 845, 847, 5, 209, 0, 0, 846, 848, 7, 13, 0, 0, 847, 846, 1, 0, 0, 0, 847, 848, 1, 0, 0, 0, 848, 850, 1, 0, 0, 0, 849, 841, 1, 0, 0, 0, 849, 842, 1, 0, 0, 0, 849, 844, 1, 0, 0, 0, 850, 103, 1, 0, 0, 0, 851, 853, 7, 14, 0, 0, 852, 851, 1, 0, 0, 0, 852, 853, 1, 0, 0, 0, 853, 860, 1, 0, 0, 0, 854, 861, 3, 102, 51, 0, 855, 861, 5, 195, 0, 0, 856, 861, 5, 196, 0, 0, 857, 861, 5, 197, 0, 0, 858, 861, 5, 81, 0, 0, 859, 861, 5, 112, 0, 0, 860, 854, 1, 0, 0, 0, 860, 855, 1, 0, 0, 0, 860, 856, 1, 0, 0, 0, 860, 857, 1, 0, 0, 0, 860, 858, 1, 0, 0, 0, 860, 859, 1, 0, 0, 0, 861, 105, 1, 0, 0, 0, 862, 866, 3, 104, 52, 0, 863, 866, 5, 198, 0, 0, 864, 866, 5, 115, 0, 0, 865, 862, 1, 0, 0, 0, 865, 863, 1, 0, 0, 0, 865, 864, 1, 0, 0, 0, 866, 107, 1, 0, 0, 0, 867, 868, 7, 15, 0, 0, 868, 109, 1, 0, 0, 0, 869, 870, 7, 16, 0, 0, 870, 111, 1, 0, 0, 0, 871, 872, 7, 17, 0, 0, 872, 113, 1, 0, 0, 0, 873, 876, 5, 193, 0, 0, 874, 876, 3, 112, 56, 0, 875, 873, 1, 0, 0, 0, 875, 874, 1, 0, 0, 0, 876, 115, 1, 0, 0, 0, 877, 881, 5, 193, 0, 0, 878, 881, 3, 108, 54, 0, 879, 881, 3, 110, 55, 0, 880, 877, 1, 0, 0, 0, 880, 878, 1, 0, 0, 0, 880, 879, 1, 0, 0, 0, 881, 117, 1, 0, 0, 0, 882, 885, 3, 116, 58, 0, 883, 885, 5, 115, 0, 0, 884, 882, 1, 0, 0, 0, 884, 883, 1, 0, 0, 0, 885, 119, 1, 0, 0, 0, 886, 887, 5, 198, 0, 0, 887, 888, 5, 211, 0, 0, 888, 889, 3, 104, 52, 0, 889, 121, 1, 0, 0, 0, 115, 124, 134, 142, 145, 149, 152, 156, 159, 162, 165, 168, 171, 175, 179, 182, 185, 188, 191, 194, 203, 209, 236, 258, 266, 269, 275, 283, 286, 292, 294, 298, 303, 306, 309, 313, 317, 320, 322, 325, 329, 333, 336, 338, 340, 343, 348, 359, 365, 370, 377, 382, 386, 390, 395, 402, 410, 413, 416, 435, 449, 465, 477, 489, 497, 501, 508, 514, 522, 527, 536, 540, 571, 588, 600, 610, 613, 617, 620, 632, 649, 653, 659, 666, 678, 681, 685, 688, 699, 723, 732, 734, 736, 744, 749, 757, 767, 770, 779, 782, 789, 799, 805, 809, 815, 822, 831, 837, 847, 849, 852, 860, 865, 875, 880, 884] \ No newline at end of file +[4, 1, 234, 883, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 2, 9, 7, 9, 2, 10, 7, 10, 2, 11, 7, 11, 2, 12, 7, 12, 2, 13, 7, 13, 2, 14, 7, 14, 2, 15, 7, 15, 2, 16, 7, 16, 2, 17, 7, 17, 2, 18, 7, 18, 2, 19, 7, 19, 2, 20, 7, 20, 2, 21, 7, 21, 2, 22, 7, 22, 2, 23, 7, 23, 2, 24, 7, 24, 2, 25, 7, 25, 2, 26, 7, 26, 2, 27, 7, 27, 2, 28, 7, 28, 2, 29, 7, 29, 2, 30, 7, 30, 2, 31, 7, 31, 2, 32, 7, 32, 2, 33, 7, 33, 2, 34, 7, 34, 2, 35, 7, 35, 2, 36, 7, 36, 2, 37, 7, 37, 2, 38, 7, 38, 2, 39, 7, 39, 2, 40, 7, 40, 2, 41, 7, 41, 2, 42, 7, 42, 2, 43, 7, 43, 2, 44, 7, 44, 2, 45, 7, 45, 2, 46, 7, 46, 2, 47, 7, 47, 2, 48, 7, 48, 2, 49, 7, 49, 2, 50, 7, 50, 2, 51, 7, 51, 2, 52, 7, 52, 2, 53, 7, 53, 2, 54, 7, 54, 2, 55, 7, 55, 2, 56, 7, 56, 2, 57, 7, 57, 2, 58, 7, 58, 2, 59, 7, 59, 1, 0, 1, 0, 3, 0, 123, 8, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 131, 8, 1, 10, 1, 12, 1, 134, 9, 1, 1, 2, 1, 2, 1, 2, 1, 2, 1, 2, 3, 2, 141, 8, 2, 1, 3, 3, 3, 144, 8, 3, 1, 3, 1, 3, 3, 3, 148, 8, 3, 1, 3, 3, 3, 151, 8, 3, 1, 3, 1, 3, 3, 3, 155, 8, 3, 1, 3, 3, 3, 158, 8, 3, 1, 3, 3, 3, 161, 8, 3, 1, 3, 3, 3, 164, 8, 3, 1, 3, 3, 3, 167, 8, 3, 1, 3, 3, 3, 170, 8, 3, 1, 3, 1, 3, 3, 3, 174, 8, 3, 1, 3, 1, 3, 3, 3, 178, 8, 3, 1, 3, 3, 3, 181, 8, 3, 1, 3, 3, 3, 184, 8, 3, 1, 3, 3, 3, 187, 8, 3, 1, 3, 3, 3, 190, 8, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 5, 3, 5, 199, 8, 5, 1, 6, 1, 6, 1, 6, 1, 7, 3, 7, 205, 8, 7, 1, 7, 1, 7, 1, 7, 1, 7, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 8, 1, 9, 1, 9, 1, 9, 1, 10, 1, 10, 1, 10, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 1, 11, 3, 11, 232, 8, 11, 1, 12, 1, 12, 1, 12, 1, 13, 1, 13, 1, 13, 1, 13, 1, 14, 1, 14, 1, 14, 1, 14, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 1, 15, 3, 15, 251, 8, 15, 1, 16, 1, 16, 1, 16, 1, 17, 1, 17, 1, 17, 3, 17, 259, 8, 17, 1, 17, 3, 17, 262, 8, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 268, 8, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 1, 17, 3, 17, 276, 8, 17, 1, 17, 3, 17, 279, 8, 17, 1, 17, 1, 17, 1, 17, 1, 17, 5, 17, 285, 8, 17, 10, 17, 12, 17, 288, 9, 17, 1, 18, 3, 18, 291, 8, 18, 1, 18, 1, 18, 1, 18, 3, 18, 296, 8, 18, 1, 18, 3, 18, 299, 8, 18, 1, 18, 3, 18, 302, 8, 18, 1, 18, 1, 18, 3, 18, 306, 8, 18, 1, 18, 1, 18, 3, 18, 310, 8, 18, 1, 18, 3, 18, 313, 8, 18, 3, 18, 315, 8, 18, 1, 18, 3, 18, 318, 8, 18, 1, 18, 1, 18, 3, 18, 322, 8, 18, 1, 18, 1, 18, 3, 18, 326, 8, 18, 1, 18, 3, 18, 329, 8, 18, 3, 18, 331, 8, 18, 3, 18, 333, 8, 18, 1, 19, 3, 19, 336, 8, 19, 1, 19, 1, 19, 1, 19, 3, 19, 341, 8, 19, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 1, 20, 3, 20, 352, 8, 20, 1, 21, 1, 21, 1, 21, 1, 21, 3, 21, 358, 8, 21, 1, 22, 1, 22, 1, 22, 3, 22, 363, 8, 22, 1, 23, 1, 23, 1, 23, 5, 23, 368, 8, 23, 10, 23, 12, 23, 371, 9, 23, 1, 24, 1, 24, 3, 24, 375, 8, 24, 1, 24, 1, 24, 3, 24, 379, 8, 24, 1, 24, 1, 24, 3, 24, 383, 8, 24, 1, 25, 1, 25, 1, 25, 3, 25, 388, 8, 25, 1, 26, 1, 26, 1, 26, 5, 26, 393, 8, 26, 10, 26, 12, 26, 396, 9, 26, 1, 27, 1, 27, 1, 27, 1, 27, 1, 28, 3, 28, 403, 8, 28, 1, 28, 3, 28, 406, 8, 28, 1, 28, 3, 28, 409, 8, 28, 1, 29, 1, 29, 1, 29, 1, 29, 1, 30, 1, 30, 1, 30, 1, 30, 1, 31, 1, 31, 1, 31, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 1, 32, 3, 32, 428, 8, 32, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 1, 33, 3, 33, 442, 8, 33, 1, 34, 1, 34, 1, 34, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 456, 8, 35, 10, 35, 12, 35, 459, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 468, 8, 35, 10, 35, 12, 35, 471, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 5, 35, 480, 8, 35, 10, 35, 12, 35, 483, 9, 35, 1, 35, 1, 35, 1, 35, 1, 35, 1, 35, 3, 35, 490, 8, 35, 1, 35, 1, 35, 3, 35, 494, 8, 35, 1, 36, 1, 36, 1, 36, 5, 36, 499, 8, 36, 10, 36, 12, 36, 502, 9, 36, 1, 37, 1, 37, 1, 37, 3, 37, 507, 8, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 1, 37, 3, 37, 515, 8, 37, 1, 38, 1, 38, 1, 38, 3, 38, 520, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 4, 38, 527, 8, 38, 11, 38, 12, 38, 528, 1, 38, 1, 38, 3, 38, 533, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 564, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 581, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 593, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 603, 8, 38, 1, 38, 3, 38, 606, 8, 38, 1, 38, 1, 38, 3, 38, 610, 8, 38, 1, 38, 3, 38, 613, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 625, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 642, 8, 38, 1, 38, 1, 38, 3, 38, 646, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 652, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 659, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 671, 8, 38, 1, 38, 3, 38, 674, 8, 38, 1, 38, 1, 38, 3, 38, 678, 8, 38, 1, 38, 3, 38, 681, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 692, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 716, 8, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 1, 38, 3, 38, 725, 8, 38, 5, 38, 727, 8, 38, 10, 38, 12, 38, 730, 9, 38, 1, 39, 1, 39, 1, 39, 5, 39, 735, 8, 39, 10, 39, 12, 39, 738, 9, 39, 1, 40, 1, 40, 3, 40, 742, 8, 40, 1, 41, 1, 41, 1, 41, 1, 41, 5, 41, 748, 8, 41, 10, 41, 12, 41, 751, 9, 41, 1, 41, 1, 41, 1, 41, 1, 41, 1, 41, 5, 41, 758, 8, 41, 10, 41, 12, 41, 761, 9, 41, 3, 41, 763, 8, 41, 1, 41, 1, 41, 1, 41, 1, 42, 1, 42, 1, 42, 1, 42, 3, 42, 772, 8, 42, 1, 42, 3, 42, 775, 8, 42, 1, 43, 1, 43, 1, 43, 5, 43, 780, 8, 43, 10, 43, 12, 43, 783, 9, 43, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 792, 8, 44, 1, 44, 1, 44, 1, 44, 1, 44, 3, 44, 798, 8, 44, 5, 44, 800, 8, 44, 10, 44, 12, 44, 803, 9, 44, 1, 45, 1, 45, 1, 45, 3, 45, 808, 8, 45, 1, 45, 1, 45, 1, 46, 1, 46, 1, 46, 3, 46, 815, 8, 46, 1, 46, 1, 46, 1, 47, 1, 47, 1, 47, 5, 47, 822, 8, 47, 10, 47, 12, 47, 825, 9, 47, 1, 48, 1, 48, 1, 48, 3, 48, 830, 8, 48, 1, 49, 1, 49, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 1, 50, 3, 50, 840, 8, 50, 3, 50, 842, 8, 50, 1, 51, 3, 51, 845, 8, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 1, 51, 3, 51, 853, 8, 51, 1, 52, 1, 52, 1, 52, 3, 52, 858, 8, 52, 1, 53, 1, 53, 1, 54, 1, 54, 1, 55, 1, 55, 1, 56, 1, 56, 3, 56, 868, 8, 56, 1, 57, 1, 57, 1, 57, 3, 57, 873, 8, 57, 1, 58, 1, 58, 3, 58, 877, 8, 58, 1, 59, 1, 59, 1, 59, 1, 59, 1, 59, 0, 3, 34, 76, 88, 60, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116, 118, 0, 18, 2, 0, 31, 31, 140, 140, 2, 0, 83, 83, 95, 95, 2, 0, 70, 70, 100, 100, 3, 0, 4, 4, 8, 8, 12, 12, 4, 0, 4, 4, 7, 8, 12, 12, 146, 146, 2, 0, 95, 95, 139, 139, 2, 0, 4, 4, 8, 8, 2, 0, 117, 117, 205, 205, 2, 0, 11, 11, 41, 42, 2, 0, 61, 61, 92, 92, 2, 0, 132, 132, 142, 142, 3, 0, 17, 17, 94, 94, 169, 169, 2, 0, 78, 78, 97, 97, 1, 0, 195, 196, 2, 0, 207, 207, 222, 222, 8, 0, 36, 36, 75, 75, 107, 107, 109, 109, 131, 131, 144, 144, 184, 184, 189, 189, 12, 0, 2, 35, 37, 74, 76, 80, 82, 106, 108, 108, 110, 111, 113, 114, 116, 129, 132, 143, 145, 183, 185, 188, 190, 191, 4, 0, 35, 35, 61, 61, 76, 76, 90, 90, 993, 0, 122, 1, 0, 0, 0, 2, 126, 1, 0, 0, 0, 4, 140, 1, 0, 0, 0, 6, 143, 1, 0, 0, 0, 8, 191, 1, 0, 0, 0, 10, 194, 1, 0, 0, 0, 12, 200, 1, 0, 0, 0, 14, 204, 1, 0, 0, 0, 16, 210, 1, 0, 0, 0, 18, 217, 1, 0, 0, 0, 20, 220, 1, 0, 0, 0, 22, 223, 1, 0, 0, 0, 24, 233, 1, 0, 0, 0, 26, 236, 1, 0, 0, 0, 28, 240, 1, 0, 0, 0, 30, 244, 1, 0, 0, 0, 32, 252, 1, 0, 0, 0, 34, 267, 1, 0, 0, 0, 36, 332, 1, 0, 0, 0, 38, 340, 1, 0, 0, 0, 40, 351, 1, 0, 0, 0, 42, 353, 1, 0, 0, 0, 44, 359, 1, 0, 0, 0, 46, 364, 1, 0, 0, 0, 48, 372, 1, 0, 0, 0, 50, 384, 1, 0, 0, 0, 52, 389, 1, 0, 0, 0, 54, 397, 1, 0, 0, 0, 56, 402, 1, 0, 0, 0, 58, 410, 1, 0, 0, 0, 60, 414, 1, 0, 0, 0, 62, 418, 1, 0, 0, 0, 64, 427, 1, 0, 0, 0, 66, 441, 1, 0, 0, 0, 68, 443, 1, 0, 0, 0, 70, 493, 1, 0, 0, 0, 72, 495, 1, 0, 0, 0, 74, 514, 1, 0, 0, 0, 76, 645, 1, 0, 0, 0, 78, 731, 1, 0, 0, 0, 80, 741, 1, 0, 0, 0, 82, 762, 1, 0, 0, 0, 84, 774, 1, 0, 0, 0, 86, 776, 1, 0, 0, 0, 88, 791, 1, 0, 0, 0, 90, 804, 1, 0, 0, 0, 92, 814, 1, 0, 0, 0, 94, 818, 1, 0, 0, 0, 96, 829, 1, 0, 0, 0, 98, 831, 1, 0, 0, 0, 100, 841, 1, 0, 0, 0, 102, 844, 1, 0, 0, 0, 104, 857, 1, 0, 0, 0, 106, 859, 1, 0, 0, 0, 108, 861, 1, 0, 0, 0, 110, 863, 1, 0, 0, 0, 112, 867, 1, 0, 0, 0, 114, 872, 1, 0, 0, 0, 116, 876, 1, 0, 0, 0, 118, 878, 1, 0, 0, 0, 120, 123, 3, 2, 1, 0, 121, 123, 3, 6, 3, 0, 122, 120, 1, 0, 0, 0, 122, 121, 1, 0, 0, 0, 123, 124, 1, 0, 0, 0, 124, 125, 5, 0, 0, 1, 125, 1, 1, 0, 0, 0, 126, 132, 3, 4, 2, 0, 127, 128, 5, 175, 0, 0, 128, 129, 5, 4, 0, 0, 129, 131, 3, 4, 2, 0, 130, 127, 1, 0, 0, 0, 131, 134, 1, 0, 0, 0, 132, 130, 1, 0, 0, 0, 132, 133, 1, 0, 0, 0, 133, 3, 1, 0, 0, 0, 134, 132, 1, 0, 0, 0, 135, 141, 3, 6, 3, 0, 136, 137, 5, 218, 0, 0, 137, 138, 3, 2, 1, 0, 138, 139, 5, 228, 0, 0, 139, 141, 1, 0, 0, 0, 140, 135, 1, 0, 0, 0, 140, 136, 1, 0, 0, 0, 141, 5, 1, 0, 0, 0, 142, 144, 3, 8, 4, 0, 143, 142, 1, 0, 0, 0, 143, 144, 1, 0, 0, 0, 144, 145, 1, 0, 0, 0, 145, 147, 5, 145, 0, 0, 146, 148, 5, 48, 0, 0, 147, 146, 1, 0, 0, 0, 147, 148, 1, 0, 0, 0, 148, 150, 1, 0, 0, 0, 149, 151, 3, 10, 5, 0, 150, 149, 1, 0, 0, 0, 150, 151, 1, 0, 0, 0, 151, 152, 1, 0, 0, 0, 152, 154, 3, 72, 36, 0, 153, 155, 3, 12, 6, 0, 154, 153, 1, 0, 0, 0, 154, 155, 1, 0, 0, 0, 155, 157, 1, 0, 0, 0, 156, 158, 3, 14, 7, 0, 157, 156, 1, 0, 0, 0, 157, 158, 1, 0, 0, 0, 158, 160, 1, 0, 0, 0, 159, 161, 3, 16, 8, 0, 160, 159, 1, 0, 0, 0, 160, 161, 1, 0, 0, 0, 161, 163, 1, 0, 0, 0, 162, 164, 3, 18, 9, 0, 163, 162, 1, 0, 0, 0, 163, 164, 1, 0, 0, 0, 164, 166, 1, 0, 0, 0, 165, 167, 3, 20, 10, 0, 166, 165, 1, 0, 0, 0, 166, 167, 1, 0, 0, 0, 167, 169, 1, 0, 0, 0, 168, 170, 3, 22, 11, 0, 169, 168, 1, 0, 0, 0, 169, 170, 1, 0, 0, 0, 170, 173, 1, 0, 0, 0, 171, 172, 5, 188, 0, 0, 172, 174, 7, 0, 0, 0, 173, 171, 1, 0, 0, 0, 173, 174, 1, 0, 0, 0, 174, 177, 1, 0, 0, 0, 175, 176, 5, 188, 0, 0, 176, 178, 5, 168, 0, 0, 177, 175, 1, 0, 0, 0, 177, 178, 1, 0, 0, 0, 178, 180, 1, 0, 0, 0, 179, 181, 3, 24, 12, 0, 180, 179, 1, 0, 0, 0, 180, 181, 1, 0, 0, 0, 181, 183, 1, 0, 0, 0, 182, 184, 3, 26, 13, 0, 183, 182, 1, 0, 0, 0, 183, 184, 1, 0, 0, 0, 184, 186, 1, 0, 0, 0, 185, 187, 3, 30, 15, 0, 186, 185, 1, 0, 0, 0, 186, 187, 1, 0, 0, 0, 187, 189, 1, 0, 0, 0, 188, 190, 3, 32, 16, 0, 189, 188, 1, 0, 0, 0, 189, 190, 1, 0, 0, 0, 190, 7, 1, 0, 0, 0, 191, 192, 5, 188, 0, 0, 192, 193, 3, 72, 36, 0, 193, 9, 1, 0, 0, 0, 194, 195, 5, 167, 0, 0, 195, 198, 5, 196, 0, 0, 196, 197, 5, 188, 0, 0, 197, 199, 5, 163, 0, 0, 198, 196, 1, 0, 0, 0, 198, 199, 1, 0, 0, 0, 199, 11, 1, 0, 0, 0, 200, 201, 5, 67, 0, 0, 201, 202, 3, 34, 17, 0, 202, 13, 1, 0, 0, 0, 203, 205, 7, 1, 0, 0, 204, 203, 1, 0, 0, 0, 204, 205, 1, 0, 0, 0, 205, 206, 1, 0, 0, 0, 206, 207, 5, 9, 0, 0, 207, 208, 5, 89, 0, 0, 208, 209, 3, 72, 36, 0, 209, 15, 1, 0, 0, 0, 210, 211, 5, 187, 0, 0, 211, 212, 3, 114, 57, 0, 212, 213, 5, 10, 0, 0, 213, 214, 5, 218, 0, 0, 214, 215, 3, 56, 28, 0, 215, 216, 5, 228, 0, 0, 216, 17, 1, 0, 0, 0, 217, 218, 5, 128, 0, 0, 218, 219, 3, 76, 38, 0, 219, 19, 1, 0, 0, 0, 220, 221, 5, 186, 0, 0, 221, 222, 3, 76, 38, 0, 222, 21, 1, 0, 0, 0, 223, 224, 5, 72, 0, 0, 224, 231, 5, 18, 0, 0, 225, 226, 7, 0, 0, 0, 226, 227, 5, 218, 0, 0, 227, 228, 3, 72, 36, 0, 228, 229, 5, 228, 0, 0, 229, 232, 1, 0, 0, 0, 230, 232, 3, 72, 36, 0, 231, 225, 1, 0, 0, 0, 231, 230, 1, 0, 0, 0, 232, 23, 1, 0, 0, 0, 233, 234, 5, 73, 0, 0, 234, 235, 3, 76, 38, 0, 235, 25, 1, 0, 0, 0, 236, 237, 5, 121, 0, 0, 237, 238, 5, 18, 0, 0, 238, 239, 3, 46, 23, 0, 239, 27, 1, 0, 0, 0, 240, 241, 5, 121, 0, 0, 241, 242, 5, 18, 0, 0, 242, 243, 3, 72, 36, 0, 243, 29, 1, 0, 0, 0, 244, 245, 5, 98, 0, 0, 245, 250, 3, 44, 22, 0, 246, 247, 5, 188, 0, 0, 247, 251, 5, 163, 0, 0, 248, 249, 5, 18, 0, 0, 249, 251, 3, 72, 36, 0, 250, 246, 1, 0, 0, 0, 250, 248, 1, 0, 0, 0, 250, 251, 1, 0, 0, 0, 251, 31, 1, 0, 0, 0, 252, 253, 5, 149, 0, 0, 253, 254, 3, 52, 26, 0, 254, 33, 1, 0, 0, 0, 255, 256, 6, 17, -1, 0, 256, 258, 3, 88, 44, 0, 257, 259, 5, 60, 0, 0, 258, 257, 1, 0, 0, 0, 258, 259, 1, 0, 0, 0, 259, 261, 1, 0, 0, 0, 260, 262, 3, 42, 21, 0, 261, 260, 1, 0, 0, 0, 261, 262, 1, 0, 0, 0, 262, 268, 1, 0, 0, 0, 263, 264, 5, 218, 0, 0, 264, 265, 3, 34, 17, 0, 265, 266, 5, 228, 0, 0, 266, 268, 1, 0, 0, 0, 267, 255, 1, 0, 0, 0, 267, 263, 1, 0, 0, 0, 268, 286, 1, 0, 0, 0, 269, 270, 10, 3, 0, 0, 270, 271, 3, 38, 19, 0, 271, 272, 3, 34, 17, 4, 272, 285, 1, 0, 0, 0, 273, 275, 10, 4, 0, 0, 274, 276, 7, 2, 0, 0, 275, 274, 1, 0, 0, 0, 275, 276, 1, 0, 0, 0, 276, 278, 1, 0, 0, 0, 277, 279, 3, 36, 18, 0, 278, 277, 1, 0, 0, 0, 278, 279, 1, 0, 0, 0, 279, 280, 1, 0, 0, 0, 280, 281, 5, 89, 0, 0, 281, 282, 3, 34, 17, 0, 282, 283, 3, 40, 20, 0, 283, 285, 1, 0, 0, 0, 284, 269, 1, 0, 0, 0, 284, 273, 1, 0, 0, 0, 285, 288, 1, 0, 0, 0, 286, 284, 1, 0, 0, 0, 286, 287, 1, 0, 0, 0, 287, 35, 1, 0, 0, 0, 288, 286, 1, 0, 0, 0, 289, 291, 7, 3, 0, 0, 290, 289, 1, 0, 0, 0, 290, 291, 1, 0, 0, 0, 291, 292, 1, 0, 0, 0, 292, 299, 5, 83, 0, 0, 293, 295, 5, 83, 0, 0, 294, 296, 7, 3, 0, 0, 295, 294, 1, 0, 0, 0, 295, 296, 1, 0, 0, 0, 296, 299, 1, 0, 0, 0, 297, 299, 7, 3, 0, 0, 298, 290, 1, 0, 0, 0, 298, 293, 1, 0, 0, 0, 298, 297, 1, 0, 0, 0, 299, 333, 1, 0, 0, 0, 300, 302, 7, 4, 0, 0, 301, 300, 1, 0, 0, 0, 301, 302, 1, 0, 0, 0, 302, 303, 1, 0, 0, 0, 303, 305, 7, 5, 0, 0, 304, 306, 5, 122, 0, 0, 305, 304, 1, 0, 0, 0, 305, 306, 1, 0, 0, 0, 306, 315, 1, 0, 0, 0, 307, 309, 7, 5, 0, 0, 308, 310, 5, 122, 0, 0, 309, 308, 1, 0, 0, 0, 309, 310, 1, 0, 0, 0, 310, 312, 1, 0, 0, 0, 311, 313, 7, 4, 0, 0, 312, 311, 1, 0, 0, 0, 312, 313, 1, 0, 0, 0, 313, 315, 1, 0, 0, 0, 314, 301, 1, 0, 0, 0, 314, 307, 1, 0, 0, 0, 315, 333, 1, 0, 0, 0, 316, 318, 7, 6, 0, 0, 317, 316, 1, 0, 0, 0, 317, 318, 1, 0, 0, 0, 318, 319, 1, 0, 0, 0, 319, 321, 5, 68, 0, 0, 320, 322, 5, 122, 0, 0, 321, 320, 1, 0, 0, 0, 321, 322, 1, 0, 0, 0, 322, 331, 1, 0, 0, 0, 323, 325, 5, 68, 0, 0, 324, 326, 5, 122, 0, 0, 325, 324, 1, 0, 0, 0, 325, 326, 1, 0, 0, 0, 326, 328, 1, 0, 0, 0, 327, 329, 7, 6, 0, 0, 328, 327, 1, 0, 0, 0, 328, 329, 1, 0, 0, 0, 329, 331, 1, 0, 0, 0, 330, 317, 1, 0, 0, 0, 330, 323, 1, 0, 0, 0, 331, 333, 1, 0, 0, 0, 332, 298, 1, 0, 0, 0, 332, 314, 1, 0, 0, 0, 332, 330, 1, 0, 0, 0, 333, 37, 1, 0, 0, 0, 334, 336, 7, 2, 0, 0, 335, 334, 1, 0, 0, 0, 335, 336, 1, 0, 0, 0, 336, 337, 1, 0, 0, 0, 337, 338, 5, 30, 0, 0, 338, 341, 5, 89, 0, 0, 339, 341, 5, 205, 0, 0, 340, 335, 1, 0, 0, 0, 340, 339, 1, 0, 0, 0, 341, 39, 1, 0, 0, 0, 342, 343, 5, 118, 0, 0, 343, 352, 3, 72, 36, 0, 344, 345, 5, 178, 0, 0, 345, 346, 5, 218, 0, 0, 346, 347, 3, 72, 36, 0, 347, 348, 5, 228, 0, 0, 348, 352, 1, 0, 0, 0, 349, 350, 5, 178, 0, 0, 350, 352, 3, 72, 36, 0, 351, 342, 1, 0, 0, 0, 351, 344, 1, 0, 0, 0, 351, 349, 1, 0, 0, 0, 352, 41, 1, 0, 0, 0, 353, 354, 5, 143, 0, 0, 354, 357, 3, 50, 25, 0, 355, 356, 5, 117, 0, 0, 356, 358, 3, 50, 25, 0, 357, 355, 1, 0, 0, 0, 357, 358, 1, 0, 0, 0, 358, 43, 1, 0, 0, 0, 359, 362, 3, 76, 38, 0, 360, 361, 7, 7, 0, 0, 361, 363, 3, 76, 38, 0, 362, 360, 1, 0, 0, 0, 362, 363, 1, 0, 0, 0, 363, 45, 1, 0, 0, 0, 364, 369, 3, 48, 24, 0, 365, 366, 5, 205, 0, 0, 366, 368, 3, 48, 24, 0, 367, 365, 1, 0, 0, 0, 368, 371, 1, 0, 0, 0, 369, 367, 1, 0, 0, 0, 369, 370, 1, 0, 0, 0, 370, 47, 1, 0, 0, 0, 371, 369, 1, 0, 0, 0, 372, 374, 3, 76, 38, 0, 373, 375, 7, 8, 0, 0, 374, 373, 1, 0, 0, 0, 374, 375, 1, 0, 0, 0, 375, 378, 1, 0, 0, 0, 376, 377, 5, 116, 0, 0, 377, 379, 7, 9, 0, 0, 378, 376, 1, 0, 0, 0, 378, 379, 1, 0, 0, 0, 379, 382, 1, 0, 0, 0, 380, 381, 5, 25, 0, 0, 381, 383, 5, 198, 0, 0, 382, 380, 1, 0, 0, 0, 382, 383, 1, 0, 0, 0, 383, 49, 1, 0, 0, 0, 384, 387, 3, 102, 51, 0, 385, 386, 5, 230, 0, 0, 386, 388, 3, 102, 51, 0, 387, 385, 1, 0, 0, 0, 387, 388, 1, 0, 0, 0, 388, 51, 1, 0, 0, 0, 389, 394, 3, 54, 27, 0, 390, 391, 5, 205, 0, 0, 391, 393, 3, 54, 27, 0, 392, 390, 1, 0, 0, 0, 393, 396, 1, 0, 0, 0, 394, 392, 1, 0, 0, 0, 394, 395, 1, 0, 0, 0, 395, 53, 1, 0, 0, 0, 396, 394, 1, 0, 0, 0, 397, 398, 3, 114, 57, 0, 398, 399, 5, 211, 0, 0, 399, 400, 3, 104, 52, 0, 400, 55, 1, 0, 0, 0, 401, 403, 3, 58, 29, 0, 402, 401, 1, 0, 0, 0, 402, 403, 1, 0, 0, 0, 403, 405, 1, 0, 0, 0, 404, 406, 3, 60, 30, 0, 405, 404, 1, 0, 0, 0, 405, 406, 1, 0, 0, 0, 406, 408, 1, 0, 0, 0, 407, 409, 3, 62, 31, 0, 408, 407, 1, 0, 0, 0, 408, 409, 1, 0, 0, 0, 409, 57, 1, 0, 0, 0, 410, 411, 5, 125, 0, 0, 411, 412, 5, 18, 0, 0, 412, 413, 3, 72, 36, 0, 413, 59, 1, 0, 0, 0, 414, 415, 5, 121, 0, 0, 415, 416, 5, 18, 0, 0, 416, 417, 3, 46, 23, 0, 417, 61, 1, 0, 0, 0, 418, 419, 7, 10, 0, 0, 419, 420, 3, 64, 32, 0, 420, 63, 1, 0, 0, 0, 421, 428, 3, 66, 33, 0, 422, 423, 5, 16, 0, 0, 423, 424, 3, 66, 33, 0, 424, 425, 5, 6, 0, 0, 425, 426, 3, 66, 33, 0, 426, 428, 1, 0, 0, 0, 427, 421, 1, 0, 0, 0, 427, 422, 1, 0, 0, 0, 428, 65, 1, 0, 0, 0, 429, 430, 5, 32, 0, 0, 430, 442, 5, 141, 0, 0, 431, 432, 5, 174, 0, 0, 432, 442, 5, 127, 0, 0, 433, 434, 5, 174, 0, 0, 434, 442, 5, 63, 0, 0, 435, 436, 3, 102, 51, 0, 436, 437, 5, 127, 0, 0, 437, 442, 1, 0, 0, 0, 438, 439, 3, 102, 51, 0, 439, 440, 5, 63, 0, 0, 440, 442, 1, 0, 0, 0, 441, 429, 1, 0, 0, 0, 441, 431, 1, 0, 0, 0, 441, 433, 1, 0, 0, 0, 441, 435, 1, 0, 0, 0, 441, 438, 1, 0, 0, 0, 442, 67, 1, 0, 0, 0, 443, 444, 3, 76, 38, 0, 444, 445, 5, 0, 0, 1, 445, 69, 1, 0, 0, 0, 446, 494, 3, 114, 57, 0, 447, 448, 3, 114, 57, 0, 448, 449, 5, 218, 0, 0, 449, 450, 3, 114, 57, 0, 450, 457, 3, 70, 35, 0, 451, 452, 5, 205, 0, 0, 452, 453, 3, 114, 57, 0, 453, 454, 3, 70, 35, 0, 454, 456, 1, 0, 0, 0, 455, 451, 1, 0, 0, 0, 456, 459, 1, 0, 0, 0, 457, 455, 1, 0, 0, 0, 457, 458, 1, 0, 0, 0, 458, 460, 1, 0, 0, 0, 459, 457, 1, 0, 0, 0, 460, 461, 5, 228, 0, 0, 461, 494, 1, 0, 0, 0, 462, 463, 3, 114, 57, 0, 463, 464, 5, 218, 0, 0, 464, 469, 3, 118, 59, 0, 465, 466, 5, 205, 0, 0, 466, 468, 3, 118, 59, 0, 467, 465, 1, 0, 0, 0, 468, 471, 1, 0, 0, 0, 469, 467, 1, 0, 0, 0, 469, 470, 1, 0, 0, 0, 470, 472, 1, 0, 0, 0, 471, 469, 1, 0, 0, 0, 472, 473, 5, 228, 0, 0, 473, 494, 1, 0, 0, 0, 474, 475, 3, 114, 57, 0, 475, 476, 5, 218, 0, 0, 476, 481, 3, 70, 35, 0, 477, 478, 5, 205, 0, 0, 478, 480, 3, 70, 35, 0, 479, 477, 1, 0, 0, 0, 480, 483, 1, 0, 0, 0, 481, 479, 1, 0, 0, 0, 481, 482, 1, 0, 0, 0, 482, 484, 1, 0, 0, 0, 483, 481, 1, 0, 0, 0, 484, 485, 5, 228, 0, 0, 485, 494, 1, 0, 0, 0, 486, 487, 3, 114, 57, 0, 487, 489, 5, 218, 0, 0, 488, 490, 3, 72, 36, 0, 489, 488, 1, 0, 0, 0, 489, 490, 1, 0, 0, 0, 490, 491, 1, 0, 0, 0, 491, 492, 5, 228, 0, 0, 492, 494, 1, 0, 0, 0, 493, 446, 1, 0, 0, 0, 493, 447, 1, 0, 0, 0, 493, 462, 1, 0, 0, 0, 493, 474, 1, 0, 0, 0, 493, 486, 1, 0, 0, 0, 494, 71, 1, 0, 0, 0, 495, 500, 3, 74, 37, 0, 496, 497, 5, 205, 0, 0, 497, 499, 3, 74, 37, 0, 498, 496, 1, 0, 0, 0, 499, 502, 1, 0, 0, 0, 500, 498, 1, 0, 0, 0, 500, 501, 1, 0, 0, 0, 501, 73, 1, 0, 0, 0, 502, 500, 1, 0, 0, 0, 503, 504, 3, 92, 46, 0, 504, 505, 5, 209, 0, 0, 505, 507, 1, 0, 0, 0, 506, 503, 1, 0, 0, 0, 506, 507, 1, 0, 0, 0, 507, 508, 1, 0, 0, 0, 508, 515, 5, 201, 0, 0, 509, 510, 5, 218, 0, 0, 510, 511, 3, 2, 1, 0, 511, 512, 5, 228, 0, 0, 512, 515, 1, 0, 0, 0, 513, 515, 3, 76, 38, 0, 514, 506, 1, 0, 0, 0, 514, 509, 1, 0, 0, 0, 514, 513, 1, 0, 0, 0, 515, 75, 1, 0, 0, 0, 516, 517, 6, 38, -1, 0, 517, 519, 5, 19, 0, 0, 518, 520, 3, 76, 38, 0, 519, 518, 1, 0, 0, 0, 519, 520, 1, 0, 0, 0, 520, 526, 1, 0, 0, 0, 521, 522, 5, 185, 0, 0, 522, 523, 3, 76, 38, 0, 523, 524, 5, 162, 0, 0, 524, 525, 3, 76, 38, 0, 525, 527, 1, 0, 0, 0, 526, 521, 1, 0, 0, 0, 527, 528, 1, 0, 0, 0, 528, 526, 1, 0, 0, 0, 528, 529, 1, 0, 0, 0, 529, 532, 1, 0, 0, 0, 530, 531, 5, 51, 0, 0, 531, 533, 3, 76, 38, 0, 532, 530, 1, 0, 0, 0, 532, 533, 1, 0, 0, 0, 533, 534, 1, 0, 0, 0, 534, 535, 5, 52, 0, 0, 535, 646, 1, 0, 0, 0, 536, 537, 5, 20, 0, 0, 537, 538, 5, 218, 0, 0, 538, 539, 3, 76, 38, 0, 539, 540, 5, 10, 0, 0, 540, 541, 3, 70, 35, 0, 541, 542, 5, 228, 0, 0, 542, 646, 1, 0, 0, 0, 543, 544, 5, 35, 0, 0, 544, 646, 5, 198, 0, 0, 545, 546, 5, 58, 0, 0, 546, 547, 5, 218, 0, 0, 547, 548, 3, 106, 53, 0, 548, 549, 5, 67, 0, 0, 549, 550, 3, 76, 38, 0, 550, 551, 5, 228, 0, 0, 551, 646, 1, 0, 0, 0, 552, 553, 5, 85, 0, 0, 553, 554, 3, 76, 38, 0, 554, 555, 3, 106, 53, 0, 555, 646, 1, 0, 0, 0, 556, 557, 5, 154, 0, 0, 557, 558, 5, 218, 0, 0, 558, 559, 3, 76, 38, 0, 559, 560, 5, 67, 0, 0, 560, 563, 3, 76, 38, 0, 561, 562, 5, 64, 0, 0, 562, 564, 3, 76, 38, 0, 563, 561, 1, 0, 0, 0, 563, 564, 1, 0, 0, 0, 564, 565, 1, 0, 0, 0, 565, 566, 5, 228, 0, 0, 566, 646, 1, 0, 0, 0, 567, 568, 5, 165, 0, 0, 568, 646, 5, 198, 0, 0, 569, 570, 5, 170, 0, 0, 570, 571, 5, 218, 0, 0, 571, 572, 7, 11, 0, 0, 572, 573, 5, 198, 0, 0, 573, 574, 5, 67, 0, 0, 574, 575, 3, 76, 38, 0, 575, 576, 5, 228, 0, 0, 576, 646, 1, 0, 0, 0, 577, 578, 3, 114, 57, 0, 578, 580, 5, 218, 0, 0, 579, 581, 3, 72, 36, 0, 580, 579, 1, 0, 0, 0, 580, 581, 1, 0, 0, 0, 581, 582, 1, 0, 0, 0, 582, 583, 5, 228, 0, 0, 583, 584, 1, 0, 0, 0, 584, 585, 5, 124, 0, 0, 585, 586, 5, 218, 0, 0, 586, 587, 3, 56, 28, 0, 587, 588, 5, 228, 0, 0, 588, 646, 1, 0, 0, 0, 589, 590, 3, 114, 57, 0, 590, 592, 5, 218, 0, 0, 591, 593, 3, 72, 36, 0, 592, 591, 1, 0, 0, 0, 592, 593, 1, 0, 0, 0, 593, 594, 1, 0, 0, 0, 594, 595, 5, 228, 0, 0, 595, 596, 1, 0, 0, 0, 596, 597, 5, 124, 0, 0, 597, 598, 3, 114, 57, 0, 598, 646, 1, 0, 0, 0, 599, 605, 3, 114, 57, 0, 600, 602, 5, 218, 0, 0, 601, 603, 3, 72, 36, 0, 602, 601, 1, 0, 0, 0, 602, 603, 1, 0, 0, 0, 603, 604, 1, 0, 0, 0, 604, 606, 5, 228, 0, 0, 605, 600, 1, 0, 0, 0, 605, 606, 1, 0, 0, 0, 606, 607, 1, 0, 0, 0, 607, 609, 5, 218, 0, 0, 608, 610, 5, 48, 0, 0, 609, 608, 1, 0, 0, 0, 609, 610, 1, 0, 0, 0, 610, 612, 1, 0, 0, 0, 611, 613, 3, 78, 39, 0, 612, 611, 1, 0, 0, 0, 612, 613, 1, 0, 0, 0, 613, 614, 1, 0, 0, 0, 614, 615, 5, 228, 0, 0, 615, 646, 1, 0, 0, 0, 616, 646, 3, 104, 52, 0, 617, 618, 5, 207, 0, 0, 618, 646, 3, 76, 38, 17, 619, 620, 5, 114, 0, 0, 620, 646, 3, 76, 38, 12, 621, 622, 3, 92, 46, 0, 622, 623, 5, 209, 0, 0, 623, 625, 1, 0, 0, 0, 624, 621, 1, 0, 0, 0, 624, 625, 1, 0, 0, 0, 625, 626, 1, 0, 0, 0, 626, 646, 5, 201, 0, 0, 627, 628, 5, 218, 0, 0, 628, 629, 3, 2, 1, 0, 629, 630, 5, 228, 0, 0, 630, 646, 1, 0, 0, 0, 631, 632, 5, 218, 0, 0, 632, 633, 3, 76, 38, 0, 633, 634, 5, 228, 0, 0, 634, 646, 1, 0, 0, 0, 635, 636, 5, 218, 0, 0, 636, 637, 3, 72, 36, 0, 637, 638, 5, 228, 0, 0, 638, 646, 1, 0, 0, 0, 639, 641, 5, 216, 0, 0, 640, 642, 3, 72, 36, 0, 641, 640, 1, 0, 0, 0, 641, 642, 1, 0, 0, 0, 642, 643, 1, 0, 0, 0, 643, 646, 5, 227, 0, 0, 644, 646, 3, 84, 42, 0, 645, 516, 1, 0, 0, 0, 645, 536, 1, 0, 0, 0, 645, 543, 1, 0, 0, 0, 645, 545, 1, 0, 0, 0, 645, 552, 1, 0, 0, 0, 645, 556, 1, 0, 0, 0, 645, 567, 1, 0, 0, 0, 645, 569, 1, 0, 0, 0, 645, 577, 1, 0, 0, 0, 645, 589, 1, 0, 0, 0, 645, 599, 1, 0, 0, 0, 645, 616, 1, 0, 0, 0, 645, 617, 1, 0, 0, 0, 645, 619, 1, 0, 0, 0, 645, 624, 1, 0, 0, 0, 645, 627, 1, 0, 0, 0, 645, 631, 1, 0, 0, 0, 645, 635, 1, 0, 0, 0, 645, 639, 1, 0, 0, 0, 645, 644, 1, 0, 0, 0, 646, 728, 1, 0, 0, 0, 647, 651, 10, 16, 0, 0, 648, 652, 5, 201, 0, 0, 649, 652, 5, 230, 0, 0, 650, 652, 5, 221, 0, 0, 651, 648, 1, 0, 0, 0, 651, 649, 1, 0, 0, 0, 651, 650, 1, 0, 0, 0, 652, 653, 1, 0, 0, 0, 653, 727, 3, 76, 38, 17, 654, 658, 10, 15, 0, 0, 655, 659, 5, 222, 0, 0, 656, 659, 5, 207, 0, 0, 657, 659, 5, 206, 0, 0, 658, 655, 1, 0, 0, 0, 658, 656, 1, 0, 0, 0, 658, 657, 1, 0, 0, 0, 659, 660, 1, 0, 0, 0, 660, 727, 3, 76, 38, 16, 661, 680, 10, 14, 0, 0, 662, 681, 5, 210, 0, 0, 663, 681, 5, 211, 0, 0, 664, 681, 5, 220, 0, 0, 665, 681, 5, 217, 0, 0, 666, 681, 5, 212, 0, 0, 667, 681, 5, 219, 0, 0, 668, 681, 5, 213, 0, 0, 669, 671, 5, 70, 0, 0, 670, 669, 1, 0, 0, 0, 670, 671, 1, 0, 0, 0, 671, 673, 1, 0, 0, 0, 672, 674, 5, 114, 0, 0, 673, 672, 1, 0, 0, 0, 673, 674, 1, 0, 0, 0, 674, 675, 1, 0, 0, 0, 675, 681, 5, 79, 0, 0, 676, 678, 5, 114, 0, 0, 677, 676, 1, 0, 0, 0, 677, 678, 1, 0, 0, 0, 678, 679, 1, 0, 0, 0, 679, 681, 7, 12, 0, 0, 680, 662, 1, 0, 0, 0, 680, 663, 1, 0, 0, 0, 680, 664, 1, 0, 0, 0, 680, 665, 1, 0, 0, 0, 680, 666, 1, 0, 0, 0, 680, 667, 1, 0, 0, 0, 680, 668, 1, 0, 0, 0, 680, 670, 1, 0, 0, 0, 680, 677, 1, 0, 0, 0, 681, 682, 1, 0, 0, 0, 682, 727, 3, 76, 38, 15, 683, 684, 10, 11, 0, 0, 684, 685, 5, 6, 0, 0, 685, 727, 3, 76, 38, 12, 686, 687, 10, 10, 0, 0, 687, 688, 5, 120, 0, 0, 688, 727, 3, 76, 38, 11, 689, 691, 10, 9, 0, 0, 690, 692, 5, 114, 0, 0, 691, 690, 1, 0, 0, 0, 691, 692, 1, 0, 0, 0, 692, 693, 1, 0, 0, 0, 693, 694, 5, 16, 0, 0, 694, 695, 3, 76, 38, 0, 695, 696, 5, 6, 0, 0, 696, 697, 3, 76, 38, 10, 697, 727, 1, 0, 0, 0, 698, 699, 10, 8, 0, 0, 699, 700, 5, 223, 0, 0, 700, 701, 3, 76, 38, 0, 701, 702, 5, 204, 0, 0, 702, 703, 3, 76, 38, 8, 703, 727, 1, 0, 0, 0, 704, 705, 10, 19, 0, 0, 705, 706, 5, 216, 0, 0, 706, 707, 3, 76, 38, 0, 707, 708, 5, 227, 0, 0, 708, 727, 1, 0, 0, 0, 709, 710, 10, 18, 0, 0, 710, 711, 5, 209, 0, 0, 711, 727, 5, 196, 0, 0, 712, 713, 10, 13, 0, 0, 713, 715, 5, 87, 0, 0, 714, 716, 5, 114, 0, 0, 715, 714, 1, 0, 0, 0, 715, 716, 1, 0, 0, 0, 716, 717, 1, 0, 0, 0, 717, 727, 5, 115, 0, 0, 718, 724, 10, 7, 0, 0, 719, 725, 3, 112, 56, 0, 720, 721, 5, 10, 0, 0, 721, 725, 3, 114, 57, 0, 722, 723, 5, 10, 0, 0, 723, 725, 5, 198, 0, 0, 724, 719, 1, 0, 0, 0, 724, 720, 1, 0, 0, 0, 724, 722, 1, 0, 0, 0, 725, 727, 1, 0, 0, 0, 726, 647, 1, 0, 0, 0, 726, 654, 1, 0, 0, 0, 726, 661, 1, 0, 0, 0, 726, 683, 1, 0, 0, 0, 726, 686, 1, 0, 0, 0, 726, 689, 1, 0, 0, 0, 726, 698, 1, 0, 0, 0, 726, 704, 1, 0, 0, 0, 726, 709, 1, 0, 0, 0, 726, 712, 1, 0, 0, 0, 726, 718, 1, 0, 0, 0, 727, 730, 1, 0, 0, 0, 728, 726, 1, 0, 0, 0, 728, 729, 1, 0, 0, 0, 729, 77, 1, 0, 0, 0, 730, 728, 1, 0, 0, 0, 731, 736, 3, 80, 40, 0, 732, 733, 5, 205, 0, 0, 733, 735, 3, 80, 40, 0, 734, 732, 1, 0, 0, 0, 735, 738, 1, 0, 0, 0, 736, 734, 1, 0, 0, 0, 736, 737, 1, 0, 0, 0, 737, 79, 1, 0, 0, 0, 738, 736, 1, 0, 0, 0, 739, 742, 3, 82, 41, 0, 740, 742, 3, 76, 38, 0, 741, 739, 1, 0, 0, 0, 741, 740, 1, 0, 0, 0, 742, 81, 1, 0, 0, 0, 743, 744, 5, 218, 0, 0, 744, 749, 3, 114, 57, 0, 745, 746, 5, 205, 0, 0, 746, 748, 3, 114, 57, 0, 747, 745, 1, 0, 0, 0, 748, 751, 1, 0, 0, 0, 749, 747, 1, 0, 0, 0, 749, 750, 1, 0, 0, 0, 750, 752, 1, 0, 0, 0, 751, 749, 1, 0, 0, 0, 752, 753, 5, 228, 0, 0, 753, 763, 1, 0, 0, 0, 754, 759, 3, 114, 57, 0, 755, 756, 5, 205, 0, 0, 756, 758, 3, 114, 57, 0, 757, 755, 1, 0, 0, 0, 758, 761, 1, 0, 0, 0, 759, 757, 1, 0, 0, 0, 759, 760, 1, 0, 0, 0, 760, 763, 1, 0, 0, 0, 761, 759, 1, 0, 0, 0, 762, 743, 1, 0, 0, 0, 762, 754, 1, 0, 0, 0, 763, 764, 1, 0, 0, 0, 764, 765, 5, 200, 0, 0, 765, 766, 3, 76, 38, 0, 766, 83, 1, 0, 0, 0, 767, 775, 5, 199, 0, 0, 768, 769, 3, 92, 46, 0, 769, 770, 5, 209, 0, 0, 770, 772, 1, 0, 0, 0, 771, 768, 1, 0, 0, 0, 771, 772, 1, 0, 0, 0, 772, 773, 1, 0, 0, 0, 773, 775, 3, 86, 43, 0, 774, 767, 1, 0, 0, 0, 774, 771, 1, 0, 0, 0, 775, 85, 1, 0, 0, 0, 776, 781, 3, 114, 57, 0, 777, 778, 5, 209, 0, 0, 778, 780, 3, 114, 57, 0, 779, 777, 1, 0, 0, 0, 780, 783, 1, 0, 0, 0, 781, 779, 1, 0, 0, 0, 781, 782, 1, 0, 0, 0, 782, 87, 1, 0, 0, 0, 783, 781, 1, 0, 0, 0, 784, 785, 6, 44, -1, 0, 785, 792, 3, 92, 46, 0, 786, 792, 3, 90, 45, 0, 787, 788, 5, 218, 0, 0, 788, 789, 3, 2, 1, 0, 789, 790, 5, 228, 0, 0, 790, 792, 1, 0, 0, 0, 791, 784, 1, 0, 0, 0, 791, 786, 1, 0, 0, 0, 791, 787, 1, 0, 0, 0, 792, 801, 1, 0, 0, 0, 793, 797, 10, 1, 0, 0, 794, 798, 3, 112, 56, 0, 795, 796, 5, 10, 0, 0, 796, 798, 3, 114, 57, 0, 797, 794, 1, 0, 0, 0, 797, 795, 1, 0, 0, 0, 798, 800, 1, 0, 0, 0, 799, 793, 1, 0, 0, 0, 800, 803, 1, 0, 0, 0, 801, 799, 1, 0, 0, 0, 801, 802, 1, 0, 0, 0, 802, 89, 1, 0, 0, 0, 803, 801, 1, 0, 0, 0, 804, 805, 3, 114, 57, 0, 805, 807, 5, 218, 0, 0, 806, 808, 3, 94, 47, 0, 807, 806, 1, 0, 0, 0, 807, 808, 1, 0, 0, 0, 808, 809, 1, 0, 0, 0, 809, 810, 5, 228, 0, 0, 810, 91, 1, 0, 0, 0, 811, 812, 3, 98, 49, 0, 812, 813, 5, 209, 0, 0, 813, 815, 1, 0, 0, 0, 814, 811, 1, 0, 0, 0, 814, 815, 1, 0, 0, 0, 815, 816, 1, 0, 0, 0, 816, 817, 3, 114, 57, 0, 817, 93, 1, 0, 0, 0, 818, 823, 3, 96, 48, 0, 819, 820, 5, 205, 0, 0, 820, 822, 3, 96, 48, 0, 821, 819, 1, 0, 0, 0, 822, 825, 1, 0, 0, 0, 823, 821, 1, 0, 0, 0, 823, 824, 1, 0, 0, 0, 824, 95, 1, 0, 0, 0, 825, 823, 1, 0, 0, 0, 826, 830, 3, 86, 43, 0, 827, 830, 3, 90, 45, 0, 828, 830, 3, 104, 52, 0, 829, 826, 1, 0, 0, 0, 829, 827, 1, 0, 0, 0, 829, 828, 1, 0, 0, 0, 830, 97, 1, 0, 0, 0, 831, 832, 3, 114, 57, 0, 832, 99, 1, 0, 0, 0, 833, 842, 5, 194, 0, 0, 834, 835, 5, 209, 0, 0, 835, 842, 7, 13, 0, 0, 836, 837, 5, 196, 0, 0, 837, 839, 5, 209, 0, 0, 838, 840, 7, 13, 0, 0, 839, 838, 1, 0, 0, 0, 839, 840, 1, 0, 0, 0, 840, 842, 1, 0, 0, 0, 841, 833, 1, 0, 0, 0, 841, 834, 1, 0, 0, 0, 841, 836, 1, 0, 0, 0, 842, 101, 1, 0, 0, 0, 843, 845, 7, 14, 0, 0, 844, 843, 1, 0, 0, 0, 844, 845, 1, 0, 0, 0, 845, 852, 1, 0, 0, 0, 846, 853, 3, 100, 50, 0, 847, 853, 5, 195, 0, 0, 848, 853, 5, 196, 0, 0, 849, 853, 5, 197, 0, 0, 850, 853, 5, 81, 0, 0, 851, 853, 5, 112, 0, 0, 852, 846, 1, 0, 0, 0, 852, 847, 1, 0, 0, 0, 852, 848, 1, 0, 0, 0, 852, 849, 1, 0, 0, 0, 852, 850, 1, 0, 0, 0, 852, 851, 1, 0, 0, 0, 853, 103, 1, 0, 0, 0, 854, 858, 3, 102, 51, 0, 855, 858, 5, 198, 0, 0, 856, 858, 5, 115, 0, 0, 857, 854, 1, 0, 0, 0, 857, 855, 1, 0, 0, 0, 857, 856, 1, 0, 0, 0, 858, 105, 1, 0, 0, 0, 859, 860, 7, 15, 0, 0, 860, 107, 1, 0, 0, 0, 861, 862, 7, 16, 0, 0, 862, 109, 1, 0, 0, 0, 863, 864, 7, 17, 0, 0, 864, 111, 1, 0, 0, 0, 865, 868, 5, 193, 0, 0, 866, 868, 3, 110, 55, 0, 867, 865, 1, 0, 0, 0, 867, 866, 1, 0, 0, 0, 868, 113, 1, 0, 0, 0, 869, 873, 5, 193, 0, 0, 870, 873, 3, 106, 53, 0, 871, 873, 3, 108, 54, 0, 872, 869, 1, 0, 0, 0, 872, 870, 1, 0, 0, 0, 872, 871, 1, 0, 0, 0, 873, 115, 1, 0, 0, 0, 874, 877, 3, 114, 57, 0, 875, 877, 5, 115, 0, 0, 876, 874, 1, 0, 0, 0, 876, 875, 1, 0, 0, 0, 877, 117, 1, 0, 0, 0, 878, 879, 5, 198, 0, 0, 879, 880, 5, 211, 0, 0, 880, 881, 3, 102, 51, 0, 881, 119, 1, 0, 0, 0, 114, 122, 132, 140, 143, 147, 150, 154, 157, 160, 163, 166, 169, 173, 177, 180, 183, 186, 189, 198, 204, 231, 250, 258, 261, 267, 275, 278, 284, 286, 290, 295, 298, 301, 305, 309, 312, 314, 317, 321, 325, 328, 330, 332, 335, 340, 351, 357, 362, 369, 374, 378, 382, 387, 394, 402, 405, 408, 427, 441, 457, 469, 481, 489, 493, 500, 506, 514, 519, 528, 532, 563, 580, 592, 602, 605, 609, 612, 624, 641, 645, 651, 658, 670, 673, 677, 680, 691, 715, 724, 726, 728, 736, 741, 749, 759, 762, 771, 774, 781, 791, 797, 801, 807, 814, 823, 829, 839, 841, 844, 852, 857, 867, 872, 876] \ No newline at end of file diff --git a/posthog/hogql/grammar/HogQLParser.py b/posthog/hogql/grammar/HogQLParser.py index 69da91c2a9518..eca17cbe4f6a4 100644 --- a/posthog/hogql/grammar/HogQLParser.py +++ b/posthog/hogql/grammar/HogQLParser.py @@ -10,7 +10,7 @@ def serializedATN(): return [ - 4,1,234,891,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, + 4,1,234,883,2,0,7,0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6, 7,6,2,7,7,7,2,8,7,8,2,9,7,9,2,10,7,10,2,11,7,11,2,12,7,12,2,13,7, 13,2,14,7,14,2,15,7,15,2,16,7,16,2,17,7,17,2,18,7,18,2,19,7,19,2, 20,7,20,2,21,7,21,2,22,7,22,2,23,7,23,2,24,7,24,2,25,7,25,2,26,7, @@ -19,348 +19,345 @@ def serializedATN(): 39,2,40,7,40,2,41,7,41,2,42,7,42,2,43,7,43,2,44,7,44,2,45,7,45,2, 46,7,46,2,47,7,47,2,48,7,48,2,49,7,49,2,50,7,50,2,51,7,51,2,52,7, 52,2,53,7,53,2,54,7,54,2,55,7,55,2,56,7,56,2,57,7,57,2,58,7,58,2, - 59,7,59,2,60,7,60,1,0,1,0,3,0,125,8,0,1,0,1,0,1,1,1,1,1,1,1,1,5, - 1,133,8,1,10,1,12,1,136,9,1,1,2,1,2,1,2,1,2,1,2,3,2,143,8,2,1,3, - 3,3,146,8,3,1,3,1,3,3,3,150,8,3,1,3,3,3,153,8,3,1,3,1,3,3,3,157, - 8,3,1,3,3,3,160,8,3,1,3,3,3,163,8,3,1,3,3,3,166,8,3,1,3,3,3,169, - 8,3,1,3,3,3,172,8,3,1,3,1,3,3,3,176,8,3,1,3,1,3,3,3,180,8,3,1,3, - 3,3,183,8,3,1,3,3,3,186,8,3,1,3,3,3,189,8,3,1,3,3,3,192,8,3,1,3, - 3,3,195,8,3,1,4,1,4,1,4,1,5,1,5,1,5,1,5,3,5,204,8,5,1,6,1,6,1,6, - 1,7,3,7,210,8,7,1,7,1,7,1,7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,9, - 1,9,1,9,1,10,1,10,1,10,1,11,1,11,1,11,1,11,1,11,1,11,1,11,1,11,3, - 11,237,8,11,1,12,1,12,1,12,1,13,1,13,1,13,1,13,1,14,1,14,1,14,1, - 14,1,15,1,15,1,15,1,15,1,15,1,16,1,16,1,16,1,16,3,16,259,8,16,1, - 17,1,17,1,17,1,18,1,18,1,18,3,18,267,8,18,1,18,3,18,270,8,18,1,18, - 1,18,1,18,1,18,3,18,276,8,18,1,18,1,18,1,18,1,18,1,18,1,18,3,18, - 284,8,18,1,18,3,18,287,8,18,1,18,1,18,1,18,1,18,5,18,293,8,18,10, - 18,12,18,296,9,18,1,19,3,19,299,8,19,1,19,1,19,1,19,3,19,304,8,19, - 1,19,3,19,307,8,19,1,19,3,19,310,8,19,1,19,1,19,3,19,314,8,19,1, - 19,1,19,3,19,318,8,19,1,19,3,19,321,8,19,3,19,323,8,19,1,19,3,19, - 326,8,19,1,19,1,19,3,19,330,8,19,1,19,1,19,3,19,334,8,19,1,19,3, - 19,337,8,19,3,19,339,8,19,3,19,341,8,19,1,20,3,20,344,8,20,1,20, - 1,20,1,20,3,20,349,8,20,1,21,1,21,1,21,1,21,1,21,1,21,1,21,1,21, - 1,21,3,21,360,8,21,1,22,1,22,1,22,1,22,3,22,366,8,22,1,23,1,23,1, - 23,3,23,371,8,23,1,24,1,24,1,24,5,24,376,8,24,10,24,12,24,379,9, - 24,1,25,1,25,3,25,383,8,25,1,25,1,25,3,25,387,8,25,1,25,1,25,3,25, - 391,8,25,1,26,1,26,1,26,3,26,396,8,26,1,27,1,27,1,27,5,27,401,8, - 27,10,27,12,27,404,9,27,1,28,1,28,1,28,1,28,1,29,3,29,411,8,29,1, - 29,3,29,414,8,29,1,29,3,29,417,8,29,1,30,1,30,1,30,1,30,1,31,1,31, - 1,31,1,31,1,32,1,32,1,32,1,33,1,33,1,33,1,33,1,33,1,33,3,33,436, - 8,33,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34,1,34, - 3,34,450,8,34,1,35,1,35,1,35,1,36,1,36,1,36,1,36,1,36,1,36,1,36, - 1,36,1,36,5,36,464,8,36,10,36,12,36,467,9,36,1,36,1,36,1,36,1,36, - 1,36,1,36,1,36,5,36,476,8,36,10,36,12,36,479,9,36,1,36,1,36,1,36, - 1,36,1,36,1,36,1,36,5,36,488,8,36,10,36,12,36,491,9,36,1,36,1,36, - 1,36,1,36,1,36,3,36,498,8,36,1,36,1,36,3,36,502,8,36,1,37,1,37,1, - 37,5,37,507,8,37,10,37,12,37,510,9,37,1,38,1,38,1,38,3,38,515,8, - 38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,523,8,38,1,39,1,39,1,39,3, - 39,528,8,39,1,39,1,39,1,39,1,39,1,39,4,39,535,8,39,11,39,12,39,536, - 1,39,1,39,3,39,541,8,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,572,8,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 3,39,589,8,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 3,39,601,8,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,611,8, - 39,1,39,3,39,614,8,39,1,39,1,39,3,39,618,8,39,1,39,3,39,621,8,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,633,8,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39, - 1,39,1,39,3,39,650,8,39,1,39,1,39,3,39,654,8,39,1,39,1,39,1,39,1, - 39,3,39,660,8,39,1,39,1,39,1,39,1,39,1,39,3,39,667,8,39,1,39,1,39, - 1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,679,8,39,1,39,3,39, - 682,8,39,1,39,1,39,3,39,686,8,39,1,39,3,39,689,8,39,1,39,1,39,1, - 39,1,39,1,39,1,39,1,39,1,39,1,39,3,39,700,8,39,1,39,1,39,1,39,1, - 39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1,39,1, - 39,1,39,1,39,1,39,1,39,1,39,3,39,724,8,39,1,39,1,39,1,39,1,39,1, - 39,1,39,1,39,3,39,733,8,39,5,39,735,8,39,10,39,12,39,738,9,39,1, - 40,1,40,1,40,5,40,743,8,40,10,40,12,40,746,9,40,1,41,1,41,3,41,750, - 8,41,1,42,1,42,1,42,1,42,5,42,756,8,42,10,42,12,42,759,9,42,1,42, - 1,42,1,42,1,42,1,42,5,42,766,8,42,10,42,12,42,769,9,42,3,42,771, - 8,42,1,42,1,42,1,42,1,43,1,43,1,43,1,43,3,43,780,8,43,1,43,3,43, - 783,8,43,1,44,1,44,1,44,5,44,788,8,44,10,44,12,44,791,9,44,1,45, - 1,45,1,45,1,45,1,45,1,45,1,45,3,45,800,8,45,1,45,1,45,1,45,1,45, - 3,45,806,8,45,5,45,808,8,45,10,45,12,45,811,9,45,1,46,1,46,1,46, - 3,46,816,8,46,1,46,1,46,1,47,1,47,1,47,3,47,823,8,47,1,47,1,47,1, - 48,1,48,1,48,5,48,830,8,48,10,48,12,48,833,9,48,1,49,1,49,1,49,3, - 49,838,8,49,1,50,1,50,1,51,1,51,1,51,1,51,1,51,1,51,3,51,848,8,51, - 3,51,850,8,51,1,52,3,52,853,8,52,1,52,1,52,1,52,1,52,1,52,1,52,3, - 52,861,8,52,1,53,1,53,1,53,3,53,866,8,53,1,54,1,54,1,55,1,55,1,56, - 1,56,1,57,1,57,3,57,876,8,57,1,58,1,58,1,58,3,58,881,8,58,1,59,1, - 59,3,59,885,8,59,1,60,1,60,1,60,1,60,1,60,0,3,36,78,90,61,0,2,4, - 6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48, - 50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84,86,88,90,92, - 94,96,98,100,102,104,106,108,110,112,114,116,118,120,0,18,2,0,31, - 31,140,140,2,0,83,83,95,95,2,0,70,70,100,100,3,0,4,4,8,8,12,12,4, - 0,4,4,7,8,12,12,146,146,2,0,95,95,139,139,2,0,4,4,8,8,2,0,117,117, - 205,205,2,0,11,11,41,42,2,0,61,61,92,92,2,0,132,132,142,142,3,0, - 17,17,94,94,169,169,2,0,78,78,97,97,1,0,195,196,2,0,207,207,222, - 222,8,0,36,36,75,75,107,107,109,109,131,131,144,144,184,184,189, - 189,12,0,2,35,37,74,76,80,82,106,108,108,110,111,113,114,116,129, - 132,143,145,183,185,188,190,191,4,0,35,35,61,61,76,76,90,90,1000, - 0,124,1,0,0,0,2,128,1,0,0,0,4,142,1,0,0,0,6,145,1,0,0,0,8,196,1, - 0,0,0,10,199,1,0,0,0,12,205,1,0,0,0,14,209,1,0,0,0,16,215,1,0,0, - 0,18,222,1,0,0,0,20,225,1,0,0,0,22,228,1,0,0,0,24,238,1,0,0,0,26, - 241,1,0,0,0,28,245,1,0,0,0,30,249,1,0,0,0,32,254,1,0,0,0,34,260, - 1,0,0,0,36,275,1,0,0,0,38,340,1,0,0,0,40,348,1,0,0,0,42,359,1,0, - 0,0,44,361,1,0,0,0,46,367,1,0,0,0,48,372,1,0,0,0,50,380,1,0,0,0, - 52,392,1,0,0,0,54,397,1,0,0,0,56,405,1,0,0,0,58,410,1,0,0,0,60,418, - 1,0,0,0,62,422,1,0,0,0,64,426,1,0,0,0,66,435,1,0,0,0,68,449,1,0, - 0,0,70,451,1,0,0,0,72,501,1,0,0,0,74,503,1,0,0,0,76,522,1,0,0,0, - 78,653,1,0,0,0,80,739,1,0,0,0,82,749,1,0,0,0,84,770,1,0,0,0,86,782, - 1,0,0,0,88,784,1,0,0,0,90,799,1,0,0,0,92,812,1,0,0,0,94,822,1,0, - 0,0,96,826,1,0,0,0,98,837,1,0,0,0,100,839,1,0,0,0,102,849,1,0,0, - 0,104,852,1,0,0,0,106,865,1,0,0,0,108,867,1,0,0,0,110,869,1,0,0, - 0,112,871,1,0,0,0,114,875,1,0,0,0,116,880,1,0,0,0,118,884,1,0,0, - 0,120,886,1,0,0,0,122,125,3,2,1,0,123,125,3,6,3,0,124,122,1,0,0, - 0,124,123,1,0,0,0,125,126,1,0,0,0,126,127,5,0,0,1,127,1,1,0,0,0, - 128,134,3,4,2,0,129,130,5,175,0,0,130,131,5,4,0,0,131,133,3,4,2, - 0,132,129,1,0,0,0,133,136,1,0,0,0,134,132,1,0,0,0,134,135,1,0,0, - 0,135,3,1,0,0,0,136,134,1,0,0,0,137,143,3,6,3,0,138,139,5,218,0, - 0,139,140,3,2,1,0,140,141,5,228,0,0,141,143,1,0,0,0,142,137,1,0, - 0,0,142,138,1,0,0,0,143,5,1,0,0,0,144,146,3,8,4,0,145,144,1,0,0, - 0,145,146,1,0,0,0,146,147,1,0,0,0,147,149,5,145,0,0,148,150,5,48, - 0,0,149,148,1,0,0,0,149,150,1,0,0,0,150,152,1,0,0,0,151,153,3,10, - 5,0,152,151,1,0,0,0,152,153,1,0,0,0,153,154,1,0,0,0,154,156,3,74, - 37,0,155,157,3,12,6,0,156,155,1,0,0,0,156,157,1,0,0,0,157,159,1, - 0,0,0,158,160,3,14,7,0,159,158,1,0,0,0,159,160,1,0,0,0,160,162,1, - 0,0,0,161,163,3,16,8,0,162,161,1,0,0,0,162,163,1,0,0,0,163,165,1, - 0,0,0,164,166,3,18,9,0,165,164,1,0,0,0,165,166,1,0,0,0,166,168,1, - 0,0,0,167,169,3,20,10,0,168,167,1,0,0,0,168,169,1,0,0,0,169,171, - 1,0,0,0,170,172,3,22,11,0,171,170,1,0,0,0,171,172,1,0,0,0,172,175, - 1,0,0,0,173,174,5,188,0,0,174,176,7,0,0,0,175,173,1,0,0,0,175,176, - 1,0,0,0,176,179,1,0,0,0,177,178,5,188,0,0,178,180,5,168,0,0,179, - 177,1,0,0,0,179,180,1,0,0,0,180,182,1,0,0,0,181,183,3,24,12,0,182, - 181,1,0,0,0,182,183,1,0,0,0,183,185,1,0,0,0,184,186,3,26,13,0,185, - 184,1,0,0,0,185,186,1,0,0,0,186,188,1,0,0,0,187,189,3,30,15,0,188, - 187,1,0,0,0,188,189,1,0,0,0,189,191,1,0,0,0,190,192,3,32,16,0,191, - 190,1,0,0,0,191,192,1,0,0,0,192,194,1,0,0,0,193,195,3,34,17,0,194, - 193,1,0,0,0,194,195,1,0,0,0,195,7,1,0,0,0,196,197,5,188,0,0,197, - 198,3,74,37,0,198,9,1,0,0,0,199,200,5,167,0,0,200,203,5,196,0,0, - 201,202,5,188,0,0,202,204,5,163,0,0,203,201,1,0,0,0,203,204,1,0, - 0,0,204,11,1,0,0,0,205,206,5,67,0,0,206,207,3,36,18,0,207,13,1,0, - 0,0,208,210,7,1,0,0,209,208,1,0,0,0,209,210,1,0,0,0,210,211,1,0, - 0,0,211,212,5,9,0,0,212,213,5,89,0,0,213,214,3,74,37,0,214,15,1, - 0,0,0,215,216,5,187,0,0,216,217,3,116,58,0,217,218,5,10,0,0,218, - 219,5,218,0,0,219,220,3,58,29,0,220,221,5,228,0,0,221,17,1,0,0,0, - 222,223,5,128,0,0,223,224,3,78,39,0,224,19,1,0,0,0,225,226,5,186, - 0,0,226,227,3,78,39,0,227,21,1,0,0,0,228,229,5,72,0,0,229,236,5, - 18,0,0,230,231,7,0,0,0,231,232,5,218,0,0,232,233,3,74,37,0,233,234, - 5,228,0,0,234,237,1,0,0,0,235,237,3,74,37,0,236,230,1,0,0,0,236, - 235,1,0,0,0,237,23,1,0,0,0,238,239,5,73,0,0,239,240,3,78,39,0,240, - 25,1,0,0,0,241,242,5,121,0,0,242,243,5,18,0,0,243,244,3,48,24,0, - 244,27,1,0,0,0,245,246,5,121,0,0,246,247,5,18,0,0,247,248,3,74,37, - 0,248,29,1,0,0,0,249,250,5,98,0,0,250,251,3,46,23,0,251,252,5,18, - 0,0,252,253,3,74,37,0,253,31,1,0,0,0,254,255,5,98,0,0,255,258,3, - 46,23,0,256,257,5,188,0,0,257,259,5,163,0,0,258,256,1,0,0,0,258, - 259,1,0,0,0,259,33,1,0,0,0,260,261,5,149,0,0,261,262,3,54,27,0,262, - 35,1,0,0,0,263,264,6,18,-1,0,264,266,3,90,45,0,265,267,5,60,0,0, - 266,265,1,0,0,0,266,267,1,0,0,0,267,269,1,0,0,0,268,270,3,44,22, - 0,269,268,1,0,0,0,269,270,1,0,0,0,270,276,1,0,0,0,271,272,5,218, - 0,0,272,273,3,36,18,0,273,274,5,228,0,0,274,276,1,0,0,0,275,263, - 1,0,0,0,275,271,1,0,0,0,276,294,1,0,0,0,277,278,10,3,0,0,278,279, - 3,40,20,0,279,280,3,36,18,4,280,293,1,0,0,0,281,283,10,4,0,0,282, - 284,7,2,0,0,283,282,1,0,0,0,283,284,1,0,0,0,284,286,1,0,0,0,285, - 287,3,38,19,0,286,285,1,0,0,0,286,287,1,0,0,0,287,288,1,0,0,0,288, - 289,5,89,0,0,289,290,3,36,18,0,290,291,3,42,21,0,291,293,1,0,0,0, - 292,277,1,0,0,0,292,281,1,0,0,0,293,296,1,0,0,0,294,292,1,0,0,0, - 294,295,1,0,0,0,295,37,1,0,0,0,296,294,1,0,0,0,297,299,7,3,0,0,298, - 297,1,0,0,0,298,299,1,0,0,0,299,300,1,0,0,0,300,307,5,83,0,0,301, - 303,5,83,0,0,302,304,7,3,0,0,303,302,1,0,0,0,303,304,1,0,0,0,304, - 307,1,0,0,0,305,307,7,3,0,0,306,298,1,0,0,0,306,301,1,0,0,0,306, - 305,1,0,0,0,307,341,1,0,0,0,308,310,7,4,0,0,309,308,1,0,0,0,309, - 310,1,0,0,0,310,311,1,0,0,0,311,313,7,5,0,0,312,314,5,122,0,0,313, - 312,1,0,0,0,313,314,1,0,0,0,314,323,1,0,0,0,315,317,7,5,0,0,316, - 318,5,122,0,0,317,316,1,0,0,0,317,318,1,0,0,0,318,320,1,0,0,0,319, - 321,7,4,0,0,320,319,1,0,0,0,320,321,1,0,0,0,321,323,1,0,0,0,322, - 309,1,0,0,0,322,315,1,0,0,0,323,341,1,0,0,0,324,326,7,6,0,0,325, - 324,1,0,0,0,325,326,1,0,0,0,326,327,1,0,0,0,327,329,5,68,0,0,328, - 330,5,122,0,0,329,328,1,0,0,0,329,330,1,0,0,0,330,339,1,0,0,0,331, - 333,5,68,0,0,332,334,5,122,0,0,333,332,1,0,0,0,333,334,1,0,0,0,334, - 336,1,0,0,0,335,337,7,6,0,0,336,335,1,0,0,0,336,337,1,0,0,0,337, - 339,1,0,0,0,338,325,1,0,0,0,338,331,1,0,0,0,339,341,1,0,0,0,340, - 306,1,0,0,0,340,322,1,0,0,0,340,338,1,0,0,0,341,39,1,0,0,0,342,344, - 7,2,0,0,343,342,1,0,0,0,343,344,1,0,0,0,344,345,1,0,0,0,345,346, - 5,30,0,0,346,349,5,89,0,0,347,349,5,205,0,0,348,343,1,0,0,0,348, - 347,1,0,0,0,349,41,1,0,0,0,350,351,5,118,0,0,351,360,3,74,37,0,352, - 353,5,178,0,0,353,354,5,218,0,0,354,355,3,74,37,0,355,356,5,228, - 0,0,356,360,1,0,0,0,357,358,5,178,0,0,358,360,3,74,37,0,359,350, - 1,0,0,0,359,352,1,0,0,0,359,357,1,0,0,0,360,43,1,0,0,0,361,362,5, - 143,0,0,362,365,3,52,26,0,363,364,5,117,0,0,364,366,3,52,26,0,365, - 363,1,0,0,0,365,366,1,0,0,0,366,45,1,0,0,0,367,370,3,78,39,0,368, - 369,7,7,0,0,369,371,3,78,39,0,370,368,1,0,0,0,370,371,1,0,0,0,371, - 47,1,0,0,0,372,377,3,50,25,0,373,374,5,205,0,0,374,376,3,50,25,0, - 375,373,1,0,0,0,376,379,1,0,0,0,377,375,1,0,0,0,377,378,1,0,0,0, - 378,49,1,0,0,0,379,377,1,0,0,0,380,382,3,78,39,0,381,383,7,8,0,0, - 382,381,1,0,0,0,382,383,1,0,0,0,383,386,1,0,0,0,384,385,5,116,0, - 0,385,387,7,9,0,0,386,384,1,0,0,0,386,387,1,0,0,0,387,390,1,0,0, - 0,388,389,5,25,0,0,389,391,5,198,0,0,390,388,1,0,0,0,390,391,1,0, - 0,0,391,51,1,0,0,0,392,395,3,104,52,0,393,394,5,230,0,0,394,396, - 3,104,52,0,395,393,1,0,0,0,395,396,1,0,0,0,396,53,1,0,0,0,397,402, - 3,56,28,0,398,399,5,205,0,0,399,401,3,56,28,0,400,398,1,0,0,0,401, - 404,1,0,0,0,402,400,1,0,0,0,402,403,1,0,0,0,403,55,1,0,0,0,404,402, - 1,0,0,0,405,406,3,116,58,0,406,407,5,211,0,0,407,408,3,106,53,0, - 408,57,1,0,0,0,409,411,3,60,30,0,410,409,1,0,0,0,410,411,1,0,0,0, - 411,413,1,0,0,0,412,414,3,62,31,0,413,412,1,0,0,0,413,414,1,0,0, - 0,414,416,1,0,0,0,415,417,3,64,32,0,416,415,1,0,0,0,416,417,1,0, - 0,0,417,59,1,0,0,0,418,419,5,125,0,0,419,420,5,18,0,0,420,421,3, - 74,37,0,421,61,1,0,0,0,422,423,5,121,0,0,423,424,5,18,0,0,424,425, - 3,48,24,0,425,63,1,0,0,0,426,427,7,10,0,0,427,428,3,66,33,0,428, - 65,1,0,0,0,429,436,3,68,34,0,430,431,5,16,0,0,431,432,3,68,34,0, - 432,433,5,6,0,0,433,434,3,68,34,0,434,436,1,0,0,0,435,429,1,0,0, - 0,435,430,1,0,0,0,436,67,1,0,0,0,437,438,5,32,0,0,438,450,5,141, - 0,0,439,440,5,174,0,0,440,450,5,127,0,0,441,442,5,174,0,0,442,450, - 5,63,0,0,443,444,3,104,52,0,444,445,5,127,0,0,445,450,1,0,0,0,446, - 447,3,104,52,0,447,448,5,63,0,0,448,450,1,0,0,0,449,437,1,0,0,0, - 449,439,1,0,0,0,449,441,1,0,0,0,449,443,1,0,0,0,449,446,1,0,0,0, - 450,69,1,0,0,0,451,452,3,78,39,0,452,453,5,0,0,1,453,71,1,0,0,0, - 454,502,3,116,58,0,455,456,3,116,58,0,456,457,5,218,0,0,457,458, - 3,116,58,0,458,465,3,72,36,0,459,460,5,205,0,0,460,461,3,116,58, - 0,461,462,3,72,36,0,462,464,1,0,0,0,463,459,1,0,0,0,464,467,1,0, - 0,0,465,463,1,0,0,0,465,466,1,0,0,0,466,468,1,0,0,0,467,465,1,0, - 0,0,468,469,5,228,0,0,469,502,1,0,0,0,470,471,3,116,58,0,471,472, - 5,218,0,0,472,477,3,120,60,0,473,474,5,205,0,0,474,476,3,120,60, - 0,475,473,1,0,0,0,476,479,1,0,0,0,477,475,1,0,0,0,477,478,1,0,0, - 0,478,480,1,0,0,0,479,477,1,0,0,0,480,481,5,228,0,0,481,502,1,0, - 0,0,482,483,3,116,58,0,483,484,5,218,0,0,484,489,3,72,36,0,485,486, - 5,205,0,0,486,488,3,72,36,0,487,485,1,0,0,0,488,491,1,0,0,0,489, - 487,1,0,0,0,489,490,1,0,0,0,490,492,1,0,0,0,491,489,1,0,0,0,492, - 493,5,228,0,0,493,502,1,0,0,0,494,495,3,116,58,0,495,497,5,218,0, - 0,496,498,3,74,37,0,497,496,1,0,0,0,497,498,1,0,0,0,498,499,1,0, - 0,0,499,500,5,228,0,0,500,502,1,0,0,0,501,454,1,0,0,0,501,455,1, - 0,0,0,501,470,1,0,0,0,501,482,1,0,0,0,501,494,1,0,0,0,502,73,1,0, - 0,0,503,508,3,76,38,0,504,505,5,205,0,0,505,507,3,76,38,0,506,504, - 1,0,0,0,507,510,1,0,0,0,508,506,1,0,0,0,508,509,1,0,0,0,509,75,1, - 0,0,0,510,508,1,0,0,0,511,512,3,94,47,0,512,513,5,209,0,0,513,515, - 1,0,0,0,514,511,1,0,0,0,514,515,1,0,0,0,515,516,1,0,0,0,516,523, - 5,201,0,0,517,518,5,218,0,0,518,519,3,2,1,0,519,520,5,228,0,0,520, - 523,1,0,0,0,521,523,3,78,39,0,522,514,1,0,0,0,522,517,1,0,0,0,522, - 521,1,0,0,0,523,77,1,0,0,0,524,525,6,39,-1,0,525,527,5,19,0,0,526, - 528,3,78,39,0,527,526,1,0,0,0,527,528,1,0,0,0,528,534,1,0,0,0,529, - 530,5,185,0,0,530,531,3,78,39,0,531,532,5,162,0,0,532,533,3,78,39, - 0,533,535,1,0,0,0,534,529,1,0,0,0,535,536,1,0,0,0,536,534,1,0,0, - 0,536,537,1,0,0,0,537,540,1,0,0,0,538,539,5,51,0,0,539,541,3,78, - 39,0,540,538,1,0,0,0,540,541,1,0,0,0,541,542,1,0,0,0,542,543,5,52, - 0,0,543,654,1,0,0,0,544,545,5,20,0,0,545,546,5,218,0,0,546,547,3, - 78,39,0,547,548,5,10,0,0,548,549,3,72,36,0,549,550,5,228,0,0,550, - 654,1,0,0,0,551,552,5,35,0,0,552,654,5,198,0,0,553,554,5,58,0,0, - 554,555,5,218,0,0,555,556,3,108,54,0,556,557,5,67,0,0,557,558,3, - 78,39,0,558,559,5,228,0,0,559,654,1,0,0,0,560,561,5,85,0,0,561,562, - 3,78,39,0,562,563,3,108,54,0,563,654,1,0,0,0,564,565,5,154,0,0,565, - 566,5,218,0,0,566,567,3,78,39,0,567,568,5,67,0,0,568,571,3,78,39, - 0,569,570,5,64,0,0,570,572,3,78,39,0,571,569,1,0,0,0,571,572,1,0, - 0,0,572,573,1,0,0,0,573,574,5,228,0,0,574,654,1,0,0,0,575,576,5, - 165,0,0,576,654,5,198,0,0,577,578,5,170,0,0,578,579,5,218,0,0,579, - 580,7,11,0,0,580,581,5,198,0,0,581,582,5,67,0,0,582,583,3,78,39, - 0,583,584,5,228,0,0,584,654,1,0,0,0,585,586,3,116,58,0,586,588,5, - 218,0,0,587,589,3,74,37,0,588,587,1,0,0,0,588,589,1,0,0,0,589,590, - 1,0,0,0,590,591,5,228,0,0,591,592,1,0,0,0,592,593,5,124,0,0,593, - 594,5,218,0,0,594,595,3,58,29,0,595,596,5,228,0,0,596,654,1,0,0, - 0,597,598,3,116,58,0,598,600,5,218,0,0,599,601,3,74,37,0,600,599, - 1,0,0,0,600,601,1,0,0,0,601,602,1,0,0,0,602,603,5,228,0,0,603,604, - 1,0,0,0,604,605,5,124,0,0,605,606,3,116,58,0,606,654,1,0,0,0,607, - 613,3,116,58,0,608,610,5,218,0,0,609,611,3,74,37,0,610,609,1,0,0, - 0,610,611,1,0,0,0,611,612,1,0,0,0,612,614,5,228,0,0,613,608,1,0, - 0,0,613,614,1,0,0,0,614,615,1,0,0,0,615,617,5,218,0,0,616,618,5, - 48,0,0,617,616,1,0,0,0,617,618,1,0,0,0,618,620,1,0,0,0,619,621,3, - 80,40,0,620,619,1,0,0,0,620,621,1,0,0,0,621,622,1,0,0,0,622,623, - 5,228,0,0,623,654,1,0,0,0,624,654,3,106,53,0,625,626,5,207,0,0,626, - 654,3,78,39,17,627,628,5,114,0,0,628,654,3,78,39,12,629,630,3,94, - 47,0,630,631,5,209,0,0,631,633,1,0,0,0,632,629,1,0,0,0,632,633,1, - 0,0,0,633,634,1,0,0,0,634,654,5,201,0,0,635,636,5,218,0,0,636,637, - 3,2,1,0,637,638,5,228,0,0,638,654,1,0,0,0,639,640,5,218,0,0,640, - 641,3,78,39,0,641,642,5,228,0,0,642,654,1,0,0,0,643,644,5,218,0, - 0,644,645,3,74,37,0,645,646,5,228,0,0,646,654,1,0,0,0,647,649,5, - 216,0,0,648,650,3,74,37,0,649,648,1,0,0,0,649,650,1,0,0,0,650,651, - 1,0,0,0,651,654,5,227,0,0,652,654,3,86,43,0,653,524,1,0,0,0,653, - 544,1,0,0,0,653,551,1,0,0,0,653,553,1,0,0,0,653,560,1,0,0,0,653, - 564,1,0,0,0,653,575,1,0,0,0,653,577,1,0,0,0,653,585,1,0,0,0,653, - 597,1,0,0,0,653,607,1,0,0,0,653,624,1,0,0,0,653,625,1,0,0,0,653, - 627,1,0,0,0,653,632,1,0,0,0,653,635,1,0,0,0,653,639,1,0,0,0,653, - 643,1,0,0,0,653,647,1,0,0,0,653,652,1,0,0,0,654,736,1,0,0,0,655, - 659,10,16,0,0,656,660,5,201,0,0,657,660,5,230,0,0,658,660,5,221, - 0,0,659,656,1,0,0,0,659,657,1,0,0,0,659,658,1,0,0,0,660,661,1,0, - 0,0,661,735,3,78,39,17,662,666,10,15,0,0,663,667,5,222,0,0,664,667, - 5,207,0,0,665,667,5,206,0,0,666,663,1,0,0,0,666,664,1,0,0,0,666, - 665,1,0,0,0,667,668,1,0,0,0,668,735,3,78,39,16,669,688,10,14,0,0, - 670,689,5,210,0,0,671,689,5,211,0,0,672,689,5,220,0,0,673,689,5, - 217,0,0,674,689,5,212,0,0,675,689,5,219,0,0,676,689,5,213,0,0,677, - 679,5,70,0,0,678,677,1,0,0,0,678,679,1,0,0,0,679,681,1,0,0,0,680, - 682,5,114,0,0,681,680,1,0,0,0,681,682,1,0,0,0,682,683,1,0,0,0,683, - 689,5,79,0,0,684,686,5,114,0,0,685,684,1,0,0,0,685,686,1,0,0,0,686, - 687,1,0,0,0,687,689,7,12,0,0,688,670,1,0,0,0,688,671,1,0,0,0,688, - 672,1,0,0,0,688,673,1,0,0,0,688,674,1,0,0,0,688,675,1,0,0,0,688, - 676,1,0,0,0,688,678,1,0,0,0,688,685,1,0,0,0,689,690,1,0,0,0,690, - 735,3,78,39,15,691,692,10,11,0,0,692,693,5,6,0,0,693,735,3,78,39, - 12,694,695,10,10,0,0,695,696,5,120,0,0,696,735,3,78,39,11,697,699, - 10,9,0,0,698,700,5,114,0,0,699,698,1,0,0,0,699,700,1,0,0,0,700,701, - 1,0,0,0,701,702,5,16,0,0,702,703,3,78,39,0,703,704,5,6,0,0,704,705, - 3,78,39,10,705,735,1,0,0,0,706,707,10,8,0,0,707,708,5,223,0,0,708, - 709,3,78,39,0,709,710,5,204,0,0,710,711,3,78,39,8,711,735,1,0,0, - 0,712,713,10,19,0,0,713,714,5,216,0,0,714,715,3,78,39,0,715,716, - 5,227,0,0,716,735,1,0,0,0,717,718,10,18,0,0,718,719,5,209,0,0,719, - 735,5,196,0,0,720,721,10,13,0,0,721,723,5,87,0,0,722,724,5,114,0, - 0,723,722,1,0,0,0,723,724,1,0,0,0,724,725,1,0,0,0,725,735,5,115, - 0,0,726,732,10,7,0,0,727,733,3,114,57,0,728,729,5,10,0,0,729,733, - 3,116,58,0,730,731,5,10,0,0,731,733,5,198,0,0,732,727,1,0,0,0,732, - 728,1,0,0,0,732,730,1,0,0,0,733,735,1,0,0,0,734,655,1,0,0,0,734, - 662,1,0,0,0,734,669,1,0,0,0,734,691,1,0,0,0,734,694,1,0,0,0,734, - 697,1,0,0,0,734,706,1,0,0,0,734,712,1,0,0,0,734,717,1,0,0,0,734, - 720,1,0,0,0,734,726,1,0,0,0,735,738,1,0,0,0,736,734,1,0,0,0,736, - 737,1,0,0,0,737,79,1,0,0,0,738,736,1,0,0,0,739,744,3,82,41,0,740, - 741,5,205,0,0,741,743,3,82,41,0,742,740,1,0,0,0,743,746,1,0,0,0, - 744,742,1,0,0,0,744,745,1,0,0,0,745,81,1,0,0,0,746,744,1,0,0,0,747, - 750,3,84,42,0,748,750,3,78,39,0,749,747,1,0,0,0,749,748,1,0,0,0, - 750,83,1,0,0,0,751,752,5,218,0,0,752,757,3,116,58,0,753,754,5,205, - 0,0,754,756,3,116,58,0,755,753,1,0,0,0,756,759,1,0,0,0,757,755,1, - 0,0,0,757,758,1,0,0,0,758,760,1,0,0,0,759,757,1,0,0,0,760,761,5, - 228,0,0,761,771,1,0,0,0,762,767,3,116,58,0,763,764,5,205,0,0,764, - 766,3,116,58,0,765,763,1,0,0,0,766,769,1,0,0,0,767,765,1,0,0,0,767, - 768,1,0,0,0,768,771,1,0,0,0,769,767,1,0,0,0,770,751,1,0,0,0,770, - 762,1,0,0,0,771,772,1,0,0,0,772,773,5,200,0,0,773,774,3,78,39,0, - 774,85,1,0,0,0,775,783,5,199,0,0,776,777,3,94,47,0,777,778,5,209, - 0,0,778,780,1,0,0,0,779,776,1,0,0,0,779,780,1,0,0,0,780,781,1,0, - 0,0,781,783,3,88,44,0,782,775,1,0,0,0,782,779,1,0,0,0,783,87,1,0, - 0,0,784,789,3,116,58,0,785,786,5,209,0,0,786,788,3,116,58,0,787, - 785,1,0,0,0,788,791,1,0,0,0,789,787,1,0,0,0,789,790,1,0,0,0,790, - 89,1,0,0,0,791,789,1,0,0,0,792,793,6,45,-1,0,793,800,3,94,47,0,794, - 800,3,92,46,0,795,796,5,218,0,0,796,797,3,2,1,0,797,798,5,228,0, - 0,798,800,1,0,0,0,799,792,1,0,0,0,799,794,1,0,0,0,799,795,1,0,0, - 0,800,809,1,0,0,0,801,805,10,1,0,0,802,806,3,114,57,0,803,804,5, - 10,0,0,804,806,3,116,58,0,805,802,1,0,0,0,805,803,1,0,0,0,806,808, - 1,0,0,0,807,801,1,0,0,0,808,811,1,0,0,0,809,807,1,0,0,0,809,810, - 1,0,0,0,810,91,1,0,0,0,811,809,1,0,0,0,812,813,3,116,58,0,813,815, - 5,218,0,0,814,816,3,96,48,0,815,814,1,0,0,0,815,816,1,0,0,0,816, - 817,1,0,0,0,817,818,5,228,0,0,818,93,1,0,0,0,819,820,3,100,50,0, - 820,821,5,209,0,0,821,823,1,0,0,0,822,819,1,0,0,0,822,823,1,0,0, - 0,823,824,1,0,0,0,824,825,3,116,58,0,825,95,1,0,0,0,826,831,3,98, - 49,0,827,828,5,205,0,0,828,830,3,98,49,0,829,827,1,0,0,0,830,833, - 1,0,0,0,831,829,1,0,0,0,831,832,1,0,0,0,832,97,1,0,0,0,833,831,1, - 0,0,0,834,838,3,88,44,0,835,838,3,92,46,0,836,838,3,106,53,0,837, - 834,1,0,0,0,837,835,1,0,0,0,837,836,1,0,0,0,838,99,1,0,0,0,839,840, - 3,116,58,0,840,101,1,0,0,0,841,850,5,194,0,0,842,843,5,209,0,0,843, - 850,7,13,0,0,844,845,5,196,0,0,845,847,5,209,0,0,846,848,7,13,0, - 0,847,846,1,0,0,0,847,848,1,0,0,0,848,850,1,0,0,0,849,841,1,0,0, - 0,849,842,1,0,0,0,849,844,1,0,0,0,850,103,1,0,0,0,851,853,7,14,0, - 0,852,851,1,0,0,0,852,853,1,0,0,0,853,860,1,0,0,0,854,861,3,102, - 51,0,855,861,5,195,0,0,856,861,5,196,0,0,857,861,5,197,0,0,858,861, - 5,81,0,0,859,861,5,112,0,0,860,854,1,0,0,0,860,855,1,0,0,0,860,856, - 1,0,0,0,860,857,1,0,0,0,860,858,1,0,0,0,860,859,1,0,0,0,861,105, - 1,0,0,0,862,866,3,104,52,0,863,866,5,198,0,0,864,866,5,115,0,0,865, - 862,1,0,0,0,865,863,1,0,0,0,865,864,1,0,0,0,866,107,1,0,0,0,867, - 868,7,15,0,0,868,109,1,0,0,0,869,870,7,16,0,0,870,111,1,0,0,0,871, - 872,7,17,0,0,872,113,1,0,0,0,873,876,5,193,0,0,874,876,3,112,56, - 0,875,873,1,0,0,0,875,874,1,0,0,0,876,115,1,0,0,0,877,881,5,193, - 0,0,878,881,3,108,54,0,879,881,3,110,55,0,880,877,1,0,0,0,880,878, - 1,0,0,0,880,879,1,0,0,0,881,117,1,0,0,0,882,885,3,116,58,0,883,885, - 5,115,0,0,884,882,1,0,0,0,884,883,1,0,0,0,885,119,1,0,0,0,886,887, - 5,198,0,0,887,888,5,211,0,0,888,889,3,104,52,0,889,121,1,0,0,0,115, - 124,134,142,145,149,152,156,159,162,165,168,171,175,179,182,185, - 188,191,194,203,209,236,258,266,269,275,283,286,292,294,298,303, - 306,309,313,317,320,322,325,329,333,336,338,340,343,348,359,365, - 370,377,382,386,390,395,402,410,413,416,435,449,465,477,489,497, - 501,508,514,522,527,536,540,571,588,600,610,613,617,620,632,649, - 653,659,666,678,681,685,688,699,723,732,734,736,744,749,757,767, - 770,779,782,789,799,805,809,815,822,831,837,847,849,852,860,865, - 875,880,884 + 59,7,59,1,0,1,0,3,0,123,8,0,1,0,1,0,1,1,1,1,1,1,1,1,5,1,131,8,1, + 10,1,12,1,134,9,1,1,2,1,2,1,2,1,2,1,2,3,2,141,8,2,1,3,3,3,144,8, + 3,1,3,1,3,3,3,148,8,3,1,3,3,3,151,8,3,1,3,1,3,3,3,155,8,3,1,3,3, + 3,158,8,3,1,3,3,3,161,8,3,1,3,3,3,164,8,3,1,3,3,3,167,8,3,1,3,3, + 3,170,8,3,1,3,1,3,3,3,174,8,3,1,3,1,3,3,3,178,8,3,1,3,3,3,181,8, + 3,1,3,3,3,184,8,3,1,3,3,3,187,8,3,1,3,3,3,190,8,3,1,4,1,4,1,4,1, + 5,1,5,1,5,1,5,3,5,199,8,5,1,6,1,6,1,6,1,7,3,7,205,8,7,1,7,1,7,1, + 7,1,7,1,8,1,8,1,8,1,8,1,8,1,8,1,8,1,9,1,9,1,9,1,10,1,10,1,10,1,11, + 1,11,1,11,1,11,1,11,1,11,1,11,1,11,3,11,232,8,11,1,12,1,12,1,12, + 1,13,1,13,1,13,1,13,1,14,1,14,1,14,1,14,1,15,1,15,1,15,1,15,1,15, + 1,15,3,15,251,8,15,1,16,1,16,1,16,1,17,1,17,1,17,3,17,259,8,17,1, + 17,3,17,262,8,17,1,17,1,17,1,17,1,17,3,17,268,8,17,1,17,1,17,1,17, + 1,17,1,17,1,17,3,17,276,8,17,1,17,3,17,279,8,17,1,17,1,17,1,17,1, + 17,5,17,285,8,17,10,17,12,17,288,9,17,1,18,3,18,291,8,18,1,18,1, + 18,1,18,3,18,296,8,18,1,18,3,18,299,8,18,1,18,3,18,302,8,18,1,18, + 1,18,3,18,306,8,18,1,18,1,18,3,18,310,8,18,1,18,3,18,313,8,18,3, + 18,315,8,18,1,18,3,18,318,8,18,1,18,1,18,3,18,322,8,18,1,18,1,18, + 3,18,326,8,18,1,18,3,18,329,8,18,3,18,331,8,18,3,18,333,8,18,1,19, + 3,19,336,8,19,1,19,1,19,1,19,3,19,341,8,19,1,20,1,20,1,20,1,20,1, + 20,1,20,1,20,1,20,1,20,3,20,352,8,20,1,21,1,21,1,21,1,21,3,21,358, + 8,21,1,22,1,22,1,22,3,22,363,8,22,1,23,1,23,1,23,5,23,368,8,23,10, + 23,12,23,371,9,23,1,24,1,24,3,24,375,8,24,1,24,1,24,3,24,379,8,24, + 1,24,1,24,3,24,383,8,24,1,25,1,25,1,25,3,25,388,8,25,1,26,1,26,1, + 26,5,26,393,8,26,10,26,12,26,396,9,26,1,27,1,27,1,27,1,27,1,28,3, + 28,403,8,28,1,28,3,28,406,8,28,1,28,3,28,409,8,28,1,29,1,29,1,29, + 1,29,1,30,1,30,1,30,1,30,1,31,1,31,1,31,1,32,1,32,1,32,1,32,1,32, + 1,32,3,32,428,8,32,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33,1,33, + 1,33,1,33,1,33,3,33,442,8,33,1,34,1,34,1,34,1,35,1,35,1,35,1,35, + 1,35,1,35,1,35,1,35,1,35,5,35,456,8,35,10,35,12,35,459,9,35,1,35, + 1,35,1,35,1,35,1,35,1,35,1,35,5,35,468,8,35,10,35,12,35,471,9,35, + 1,35,1,35,1,35,1,35,1,35,1,35,1,35,5,35,480,8,35,10,35,12,35,483, + 9,35,1,35,1,35,1,35,1,35,1,35,3,35,490,8,35,1,35,1,35,3,35,494,8, + 35,1,36,1,36,1,36,5,36,499,8,36,10,36,12,36,502,9,36,1,37,1,37,1, + 37,3,37,507,8,37,1,37,1,37,1,37,1,37,1,37,1,37,3,37,515,8,37,1,38, + 1,38,1,38,3,38,520,8,38,1,38,1,38,1,38,1,38,1,38,4,38,527,8,38,11, + 38,12,38,528,1,38,1,38,3,38,533,8,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,564,8, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,3,38,581,8,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,3,38,593,8,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,3,38,603,8,38,1,38,3,38,606,8,38,1,38,1,38,3,38,610,8,38,1,38, + 3,38,613,8,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38, + 3,38,625,8,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38, + 1,38,1,38,1,38,1,38,1,38,3,38,642,8,38,1,38,1,38,3,38,646,8,38,1, + 38,1,38,1,38,1,38,3,38,652,8,38,1,38,1,38,1,38,1,38,1,38,3,38,659, + 8,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,671, + 8,38,1,38,3,38,674,8,38,1,38,1,38,3,38,678,8,38,1,38,3,38,681,8, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,692,8,38,1, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1, + 38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,1,38,3,38,716,8,38,1,38,1, + 38,1,38,1,38,1,38,1,38,1,38,3,38,725,8,38,5,38,727,8,38,10,38,12, + 38,730,9,38,1,39,1,39,1,39,5,39,735,8,39,10,39,12,39,738,9,39,1, + 40,1,40,3,40,742,8,40,1,41,1,41,1,41,1,41,5,41,748,8,41,10,41,12, + 41,751,9,41,1,41,1,41,1,41,1,41,1,41,5,41,758,8,41,10,41,12,41,761, + 9,41,3,41,763,8,41,1,41,1,41,1,41,1,42,1,42,1,42,1,42,3,42,772,8, + 42,1,42,3,42,775,8,42,1,43,1,43,1,43,5,43,780,8,43,10,43,12,43,783, + 9,43,1,44,1,44,1,44,1,44,1,44,1,44,1,44,3,44,792,8,44,1,44,1,44, + 1,44,1,44,3,44,798,8,44,5,44,800,8,44,10,44,12,44,803,9,44,1,45, + 1,45,1,45,3,45,808,8,45,1,45,1,45,1,46,1,46,1,46,3,46,815,8,46,1, + 46,1,46,1,47,1,47,1,47,5,47,822,8,47,10,47,12,47,825,9,47,1,48,1, + 48,1,48,3,48,830,8,48,1,49,1,49,1,50,1,50,1,50,1,50,1,50,1,50,3, + 50,840,8,50,3,50,842,8,50,1,51,3,51,845,8,51,1,51,1,51,1,51,1,51, + 1,51,1,51,3,51,853,8,51,1,52,1,52,1,52,3,52,858,8,52,1,53,1,53,1, + 54,1,54,1,55,1,55,1,56,1,56,3,56,868,8,56,1,57,1,57,1,57,3,57,873, + 8,57,1,58,1,58,3,58,877,8,58,1,59,1,59,1,59,1,59,1,59,0,3,34,76, + 88,60,0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40, + 42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78,80,82,84, + 86,88,90,92,94,96,98,100,102,104,106,108,110,112,114,116,118,0,18, + 2,0,31,31,140,140,2,0,83,83,95,95,2,0,70,70,100,100,3,0,4,4,8,8, + 12,12,4,0,4,4,7,8,12,12,146,146,2,0,95,95,139,139,2,0,4,4,8,8,2, + 0,117,117,205,205,2,0,11,11,41,42,2,0,61,61,92,92,2,0,132,132,142, + 142,3,0,17,17,94,94,169,169,2,0,78,78,97,97,1,0,195,196,2,0,207, + 207,222,222,8,0,36,36,75,75,107,107,109,109,131,131,144,144,184, + 184,189,189,12,0,2,35,37,74,76,80,82,106,108,108,110,111,113,114, + 116,129,132,143,145,183,185,188,190,191,4,0,35,35,61,61,76,76,90, + 90,993,0,122,1,0,0,0,2,126,1,0,0,0,4,140,1,0,0,0,6,143,1,0,0,0,8, + 191,1,0,0,0,10,194,1,0,0,0,12,200,1,0,0,0,14,204,1,0,0,0,16,210, + 1,0,0,0,18,217,1,0,0,0,20,220,1,0,0,0,22,223,1,0,0,0,24,233,1,0, + 0,0,26,236,1,0,0,0,28,240,1,0,0,0,30,244,1,0,0,0,32,252,1,0,0,0, + 34,267,1,0,0,0,36,332,1,0,0,0,38,340,1,0,0,0,40,351,1,0,0,0,42,353, + 1,0,0,0,44,359,1,0,0,0,46,364,1,0,0,0,48,372,1,0,0,0,50,384,1,0, + 0,0,52,389,1,0,0,0,54,397,1,0,0,0,56,402,1,0,0,0,58,410,1,0,0,0, + 60,414,1,0,0,0,62,418,1,0,0,0,64,427,1,0,0,0,66,441,1,0,0,0,68,443, + 1,0,0,0,70,493,1,0,0,0,72,495,1,0,0,0,74,514,1,0,0,0,76,645,1,0, + 0,0,78,731,1,0,0,0,80,741,1,0,0,0,82,762,1,0,0,0,84,774,1,0,0,0, + 86,776,1,0,0,0,88,791,1,0,0,0,90,804,1,0,0,0,92,814,1,0,0,0,94,818, + 1,0,0,0,96,829,1,0,0,0,98,831,1,0,0,0,100,841,1,0,0,0,102,844,1, + 0,0,0,104,857,1,0,0,0,106,859,1,0,0,0,108,861,1,0,0,0,110,863,1, + 0,0,0,112,867,1,0,0,0,114,872,1,0,0,0,116,876,1,0,0,0,118,878,1, + 0,0,0,120,123,3,2,1,0,121,123,3,6,3,0,122,120,1,0,0,0,122,121,1, + 0,0,0,123,124,1,0,0,0,124,125,5,0,0,1,125,1,1,0,0,0,126,132,3,4, + 2,0,127,128,5,175,0,0,128,129,5,4,0,0,129,131,3,4,2,0,130,127,1, + 0,0,0,131,134,1,0,0,0,132,130,1,0,0,0,132,133,1,0,0,0,133,3,1,0, + 0,0,134,132,1,0,0,0,135,141,3,6,3,0,136,137,5,218,0,0,137,138,3, + 2,1,0,138,139,5,228,0,0,139,141,1,0,0,0,140,135,1,0,0,0,140,136, + 1,0,0,0,141,5,1,0,0,0,142,144,3,8,4,0,143,142,1,0,0,0,143,144,1, + 0,0,0,144,145,1,0,0,0,145,147,5,145,0,0,146,148,5,48,0,0,147,146, + 1,0,0,0,147,148,1,0,0,0,148,150,1,0,0,0,149,151,3,10,5,0,150,149, + 1,0,0,0,150,151,1,0,0,0,151,152,1,0,0,0,152,154,3,72,36,0,153,155, + 3,12,6,0,154,153,1,0,0,0,154,155,1,0,0,0,155,157,1,0,0,0,156,158, + 3,14,7,0,157,156,1,0,0,0,157,158,1,0,0,0,158,160,1,0,0,0,159,161, + 3,16,8,0,160,159,1,0,0,0,160,161,1,0,0,0,161,163,1,0,0,0,162,164, + 3,18,9,0,163,162,1,0,0,0,163,164,1,0,0,0,164,166,1,0,0,0,165,167, + 3,20,10,0,166,165,1,0,0,0,166,167,1,0,0,0,167,169,1,0,0,0,168,170, + 3,22,11,0,169,168,1,0,0,0,169,170,1,0,0,0,170,173,1,0,0,0,171,172, + 5,188,0,0,172,174,7,0,0,0,173,171,1,0,0,0,173,174,1,0,0,0,174,177, + 1,0,0,0,175,176,5,188,0,0,176,178,5,168,0,0,177,175,1,0,0,0,177, + 178,1,0,0,0,178,180,1,0,0,0,179,181,3,24,12,0,180,179,1,0,0,0,180, + 181,1,0,0,0,181,183,1,0,0,0,182,184,3,26,13,0,183,182,1,0,0,0,183, + 184,1,0,0,0,184,186,1,0,0,0,185,187,3,30,15,0,186,185,1,0,0,0,186, + 187,1,0,0,0,187,189,1,0,0,0,188,190,3,32,16,0,189,188,1,0,0,0,189, + 190,1,0,0,0,190,7,1,0,0,0,191,192,5,188,0,0,192,193,3,72,36,0,193, + 9,1,0,0,0,194,195,5,167,0,0,195,198,5,196,0,0,196,197,5,188,0,0, + 197,199,5,163,0,0,198,196,1,0,0,0,198,199,1,0,0,0,199,11,1,0,0,0, + 200,201,5,67,0,0,201,202,3,34,17,0,202,13,1,0,0,0,203,205,7,1,0, + 0,204,203,1,0,0,0,204,205,1,0,0,0,205,206,1,0,0,0,206,207,5,9,0, + 0,207,208,5,89,0,0,208,209,3,72,36,0,209,15,1,0,0,0,210,211,5,187, + 0,0,211,212,3,114,57,0,212,213,5,10,0,0,213,214,5,218,0,0,214,215, + 3,56,28,0,215,216,5,228,0,0,216,17,1,0,0,0,217,218,5,128,0,0,218, + 219,3,76,38,0,219,19,1,0,0,0,220,221,5,186,0,0,221,222,3,76,38,0, + 222,21,1,0,0,0,223,224,5,72,0,0,224,231,5,18,0,0,225,226,7,0,0,0, + 226,227,5,218,0,0,227,228,3,72,36,0,228,229,5,228,0,0,229,232,1, + 0,0,0,230,232,3,72,36,0,231,225,1,0,0,0,231,230,1,0,0,0,232,23,1, + 0,0,0,233,234,5,73,0,0,234,235,3,76,38,0,235,25,1,0,0,0,236,237, + 5,121,0,0,237,238,5,18,0,0,238,239,3,46,23,0,239,27,1,0,0,0,240, + 241,5,121,0,0,241,242,5,18,0,0,242,243,3,72,36,0,243,29,1,0,0,0, + 244,245,5,98,0,0,245,250,3,44,22,0,246,247,5,188,0,0,247,251,5,163, + 0,0,248,249,5,18,0,0,249,251,3,72,36,0,250,246,1,0,0,0,250,248,1, + 0,0,0,250,251,1,0,0,0,251,31,1,0,0,0,252,253,5,149,0,0,253,254,3, + 52,26,0,254,33,1,0,0,0,255,256,6,17,-1,0,256,258,3,88,44,0,257,259, + 5,60,0,0,258,257,1,0,0,0,258,259,1,0,0,0,259,261,1,0,0,0,260,262, + 3,42,21,0,261,260,1,0,0,0,261,262,1,0,0,0,262,268,1,0,0,0,263,264, + 5,218,0,0,264,265,3,34,17,0,265,266,5,228,0,0,266,268,1,0,0,0,267, + 255,1,0,0,0,267,263,1,0,0,0,268,286,1,0,0,0,269,270,10,3,0,0,270, + 271,3,38,19,0,271,272,3,34,17,4,272,285,1,0,0,0,273,275,10,4,0,0, + 274,276,7,2,0,0,275,274,1,0,0,0,275,276,1,0,0,0,276,278,1,0,0,0, + 277,279,3,36,18,0,278,277,1,0,0,0,278,279,1,0,0,0,279,280,1,0,0, + 0,280,281,5,89,0,0,281,282,3,34,17,0,282,283,3,40,20,0,283,285,1, + 0,0,0,284,269,1,0,0,0,284,273,1,0,0,0,285,288,1,0,0,0,286,284,1, + 0,0,0,286,287,1,0,0,0,287,35,1,0,0,0,288,286,1,0,0,0,289,291,7,3, + 0,0,290,289,1,0,0,0,290,291,1,0,0,0,291,292,1,0,0,0,292,299,5,83, + 0,0,293,295,5,83,0,0,294,296,7,3,0,0,295,294,1,0,0,0,295,296,1,0, + 0,0,296,299,1,0,0,0,297,299,7,3,0,0,298,290,1,0,0,0,298,293,1,0, + 0,0,298,297,1,0,0,0,299,333,1,0,0,0,300,302,7,4,0,0,301,300,1,0, + 0,0,301,302,1,0,0,0,302,303,1,0,0,0,303,305,7,5,0,0,304,306,5,122, + 0,0,305,304,1,0,0,0,305,306,1,0,0,0,306,315,1,0,0,0,307,309,7,5, + 0,0,308,310,5,122,0,0,309,308,1,0,0,0,309,310,1,0,0,0,310,312,1, + 0,0,0,311,313,7,4,0,0,312,311,1,0,0,0,312,313,1,0,0,0,313,315,1, + 0,0,0,314,301,1,0,0,0,314,307,1,0,0,0,315,333,1,0,0,0,316,318,7, + 6,0,0,317,316,1,0,0,0,317,318,1,0,0,0,318,319,1,0,0,0,319,321,5, + 68,0,0,320,322,5,122,0,0,321,320,1,0,0,0,321,322,1,0,0,0,322,331, + 1,0,0,0,323,325,5,68,0,0,324,326,5,122,0,0,325,324,1,0,0,0,325,326, + 1,0,0,0,326,328,1,0,0,0,327,329,7,6,0,0,328,327,1,0,0,0,328,329, + 1,0,0,0,329,331,1,0,0,0,330,317,1,0,0,0,330,323,1,0,0,0,331,333, + 1,0,0,0,332,298,1,0,0,0,332,314,1,0,0,0,332,330,1,0,0,0,333,37,1, + 0,0,0,334,336,7,2,0,0,335,334,1,0,0,0,335,336,1,0,0,0,336,337,1, + 0,0,0,337,338,5,30,0,0,338,341,5,89,0,0,339,341,5,205,0,0,340,335, + 1,0,0,0,340,339,1,0,0,0,341,39,1,0,0,0,342,343,5,118,0,0,343,352, + 3,72,36,0,344,345,5,178,0,0,345,346,5,218,0,0,346,347,3,72,36,0, + 347,348,5,228,0,0,348,352,1,0,0,0,349,350,5,178,0,0,350,352,3,72, + 36,0,351,342,1,0,0,0,351,344,1,0,0,0,351,349,1,0,0,0,352,41,1,0, + 0,0,353,354,5,143,0,0,354,357,3,50,25,0,355,356,5,117,0,0,356,358, + 3,50,25,0,357,355,1,0,0,0,357,358,1,0,0,0,358,43,1,0,0,0,359,362, + 3,76,38,0,360,361,7,7,0,0,361,363,3,76,38,0,362,360,1,0,0,0,362, + 363,1,0,0,0,363,45,1,0,0,0,364,369,3,48,24,0,365,366,5,205,0,0,366, + 368,3,48,24,0,367,365,1,0,0,0,368,371,1,0,0,0,369,367,1,0,0,0,369, + 370,1,0,0,0,370,47,1,0,0,0,371,369,1,0,0,0,372,374,3,76,38,0,373, + 375,7,8,0,0,374,373,1,0,0,0,374,375,1,0,0,0,375,378,1,0,0,0,376, + 377,5,116,0,0,377,379,7,9,0,0,378,376,1,0,0,0,378,379,1,0,0,0,379, + 382,1,0,0,0,380,381,5,25,0,0,381,383,5,198,0,0,382,380,1,0,0,0,382, + 383,1,0,0,0,383,49,1,0,0,0,384,387,3,102,51,0,385,386,5,230,0,0, + 386,388,3,102,51,0,387,385,1,0,0,0,387,388,1,0,0,0,388,51,1,0,0, + 0,389,394,3,54,27,0,390,391,5,205,0,0,391,393,3,54,27,0,392,390, + 1,0,0,0,393,396,1,0,0,0,394,392,1,0,0,0,394,395,1,0,0,0,395,53,1, + 0,0,0,396,394,1,0,0,0,397,398,3,114,57,0,398,399,5,211,0,0,399,400, + 3,104,52,0,400,55,1,0,0,0,401,403,3,58,29,0,402,401,1,0,0,0,402, + 403,1,0,0,0,403,405,1,0,0,0,404,406,3,60,30,0,405,404,1,0,0,0,405, + 406,1,0,0,0,406,408,1,0,0,0,407,409,3,62,31,0,408,407,1,0,0,0,408, + 409,1,0,0,0,409,57,1,0,0,0,410,411,5,125,0,0,411,412,5,18,0,0,412, + 413,3,72,36,0,413,59,1,0,0,0,414,415,5,121,0,0,415,416,5,18,0,0, + 416,417,3,46,23,0,417,61,1,0,0,0,418,419,7,10,0,0,419,420,3,64,32, + 0,420,63,1,0,0,0,421,428,3,66,33,0,422,423,5,16,0,0,423,424,3,66, + 33,0,424,425,5,6,0,0,425,426,3,66,33,0,426,428,1,0,0,0,427,421,1, + 0,0,0,427,422,1,0,0,0,428,65,1,0,0,0,429,430,5,32,0,0,430,442,5, + 141,0,0,431,432,5,174,0,0,432,442,5,127,0,0,433,434,5,174,0,0,434, + 442,5,63,0,0,435,436,3,102,51,0,436,437,5,127,0,0,437,442,1,0,0, + 0,438,439,3,102,51,0,439,440,5,63,0,0,440,442,1,0,0,0,441,429,1, + 0,0,0,441,431,1,0,0,0,441,433,1,0,0,0,441,435,1,0,0,0,441,438,1, + 0,0,0,442,67,1,0,0,0,443,444,3,76,38,0,444,445,5,0,0,1,445,69,1, + 0,0,0,446,494,3,114,57,0,447,448,3,114,57,0,448,449,5,218,0,0,449, + 450,3,114,57,0,450,457,3,70,35,0,451,452,5,205,0,0,452,453,3,114, + 57,0,453,454,3,70,35,0,454,456,1,0,0,0,455,451,1,0,0,0,456,459,1, + 0,0,0,457,455,1,0,0,0,457,458,1,0,0,0,458,460,1,0,0,0,459,457,1, + 0,0,0,460,461,5,228,0,0,461,494,1,0,0,0,462,463,3,114,57,0,463,464, + 5,218,0,0,464,469,3,118,59,0,465,466,5,205,0,0,466,468,3,118,59, + 0,467,465,1,0,0,0,468,471,1,0,0,0,469,467,1,0,0,0,469,470,1,0,0, + 0,470,472,1,0,0,0,471,469,1,0,0,0,472,473,5,228,0,0,473,494,1,0, + 0,0,474,475,3,114,57,0,475,476,5,218,0,0,476,481,3,70,35,0,477,478, + 5,205,0,0,478,480,3,70,35,0,479,477,1,0,0,0,480,483,1,0,0,0,481, + 479,1,0,0,0,481,482,1,0,0,0,482,484,1,0,0,0,483,481,1,0,0,0,484, + 485,5,228,0,0,485,494,1,0,0,0,486,487,3,114,57,0,487,489,5,218,0, + 0,488,490,3,72,36,0,489,488,1,0,0,0,489,490,1,0,0,0,490,491,1,0, + 0,0,491,492,5,228,0,0,492,494,1,0,0,0,493,446,1,0,0,0,493,447,1, + 0,0,0,493,462,1,0,0,0,493,474,1,0,0,0,493,486,1,0,0,0,494,71,1,0, + 0,0,495,500,3,74,37,0,496,497,5,205,0,0,497,499,3,74,37,0,498,496, + 1,0,0,0,499,502,1,0,0,0,500,498,1,0,0,0,500,501,1,0,0,0,501,73,1, + 0,0,0,502,500,1,0,0,0,503,504,3,92,46,0,504,505,5,209,0,0,505,507, + 1,0,0,0,506,503,1,0,0,0,506,507,1,0,0,0,507,508,1,0,0,0,508,515, + 5,201,0,0,509,510,5,218,0,0,510,511,3,2,1,0,511,512,5,228,0,0,512, + 515,1,0,0,0,513,515,3,76,38,0,514,506,1,0,0,0,514,509,1,0,0,0,514, + 513,1,0,0,0,515,75,1,0,0,0,516,517,6,38,-1,0,517,519,5,19,0,0,518, + 520,3,76,38,0,519,518,1,0,0,0,519,520,1,0,0,0,520,526,1,0,0,0,521, + 522,5,185,0,0,522,523,3,76,38,0,523,524,5,162,0,0,524,525,3,76,38, + 0,525,527,1,0,0,0,526,521,1,0,0,0,527,528,1,0,0,0,528,526,1,0,0, + 0,528,529,1,0,0,0,529,532,1,0,0,0,530,531,5,51,0,0,531,533,3,76, + 38,0,532,530,1,0,0,0,532,533,1,0,0,0,533,534,1,0,0,0,534,535,5,52, + 0,0,535,646,1,0,0,0,536,537,5,20,0,0,537,538,5,218,0,0,538,539,3, + 76,38,0,539,540,5,10,0,0,540,541,3,70,35,0,541,542,5,228,0,0,542, + 646,1,0,0,0,543,544,5,35,0,0,544,646,5,198,0,0,545,546,5,58,0,0, + 546,547,5,218,0,0,547,548,3,106,53,0,548,549,5,67,0,0,549,550,3, + 76,38,0,550,551,5,228,0,0,551,646,1,0,0,0,552,553,5,85,0,0,553,554, + 3,76,38,0,554,555,3,106,53,0,555,646,1,0,0,0,556,557,5,154,0,0,557, + 558,5,218,0,0,558,559,3,76,38,0,559,560,5,67,0,0,560,563,3,76,38, + 0,561,562,5,64,0,0,562,564,3,76,38,0,563,561,1,0,0,0,563,564,1,0, + 0,0,564,565,1,0,0,0,565,566,5,228,0,0,566,646,1,0,0,0,567,568,5, + 165,0,0,568,646,5,198,0,0,569,570,5,170,0,0,570,571,5,218,0,0,571, + 572,7,11,0,0,572,573,5,198,0,0,573,574,5,67,0,0,574,575,3,76,38, + 0,575,576,5,228,0,0,576,646,1,0,0,0,577,578,3,114,57,0,578,580,5, + 218,0,0,579,581,3,72,36,0,580,579,1,0,0,0,580,581,1,0,0,0,581,582, + 1,0,0,0,582,583,5,228,0,0,583,584,1,0,0,0,584,585,5,124,0,0,585, + 586,5,218,0,0,586,587,3,56,28,0,587,588,5,228,0,0,588,646,1,0,0, + 0,589,590,3,114,57,0,590,592,5,218,0,0,591,593,3,72,36,0,592,591, + 1,0,0,0,592,593,1,0,0,0,593,594,1,0,0,0,594,595,5,228,0,0,595,596, + 1,0,0,0,596,597,5,124,0,0,597,598,3,114,57,0,598,646,1,0,0,0,599, + 605,3,114,57,0,600,602,5,218,0,0,601,603,3,72,36,0,602,601,1,0,0, + 0,602,603,1,0,0,0,603,604,1,0,0,0,604,606,5,228,0,0,605,600,1,0, + 0,0,605,606,1,0,0,0,606,607,1,0,0,0,607,609,5,218,0,0,608,610,5, + 48,0,0,609,608,1,0,0,0,609,610,1,0,0,0,610,612,1,0,0,0,611,613,3, + 78,39,0,612,611,1,0,0,0,612,613,1,0,0,0,613,614,1,0,0,0,614,615, + 5,228,0,0,615,646,1,0,0,0,616,646,3,104,52,0,617,618,5,207,0,0,618, + 646,3,76,38,17,619,620,5,114,0,0,620,646,3,76,38,12,621,622,3,92, + 46,0,622,623,5,209,0,0,623,625,1,0,0,0,624,621,1,0,0,0,624,625,1, + 0,0,0,625,626,1,0,0,0,626,646,5,201,0,0,627,628,5,218,0,0,628,629, + 3,2,1,0,629,630,5,228,0,0,630,646,1,0,0,0,631,632,5,218,0,0,632, + 633,3,76,38,0,633,634,5,228,0,0,634,646,1,0,0,0,635,636,5,218,0, + 0,636,637,3,72,36,0,637,638,5,228,0,0,638,646,1,0,0,0,639,641,5, + 216,0,0,640,642,3,72,36,0,641,640,1,0,0,0,641,642,1,0,0,0,642,643, + 1,0,0,0,643,646,5,227,0,0,644,646,3,84,42,0,645,516,1,0,0,0,645, + 536,1,0,0,0,645,543,1,0,0,0,645,545,1,0,0,0,645,552,1,0,0,0,645, + 556,1,0,0,0,645,567,1,0,0,0,645,569,1,0,0,0,645,577,1,0,0,0,645, + 589,1,0,0,0,645,599,1,0,0,0,645,616,1,0,0,0,645,617,1,0,0,0,645, + 619,1,0,0,0,645,624,1,0,0,0,645,627,1,0,0,0,645,631,1,0,0,0,645, + 635,1,0,0,0,645,639,1,0,0,0,645,644,1,0,0,0,646,728,1,0,0,0,647, + 651,10,16,0,0,648,652,5,201,0,0,649,652,5,230,0,0,650,652,5,221, + 0,0,651,648,1,0,0,0,651,649,1,0,0,0,651,650,1,0,0,0,652,653,1,0, + 0,0,653,727,3,76,38,17,654,658,10,15,0,0,655,659,5,222,0,0,656,659, + 5,207,0,0,657,659,5,206,0,0,658,655,1,0,0,0,658,656,1,0,0,0,658, + 657,1,0,0,0,659,660,1,0,0,0,660,727,3,76,38,16,661,680,10,14,0,0, + 662,681,5,210,0,0,663,681,5,211,0,0,664,681,5,220,0,0,665,681,5, + 217,0,0,666,681,5,212,0,0,667,681,5,219,0,0,668,681,5,213,0,0,669, + 671,5,70,0,0,670,669,1,0,0,0,670,671,1,0,0,0,671,673,1,0,0,0,672, + 674,5,114,0,0,673,672,1,0,0,0,673,674,1,0,0,0,674,675,1,0,0,0,675, + 681,5,79,0,0,676,678,5,114,0,0,677,676,1,0,0,0,677,678,1,0,0,0,678, + 679,1,0,0,0,679,681,7,12,0,0,680,662,1,0,0,0,680,663,1,0,0,0,680, + 664,1,0,0,0,680,665,1,0,0,0,680,666,1,0,0,0,680,667,1,0,0,0,680, + 668,1,0,0,0,680,670,1,0,0,0,680,677,1,0,0,0,681,682,1,0,0,0,682, + 727,3,76,38,15,683,684,10,11,0,0,684,685,5,6,0,0,685,727,3,76,38, + 12,686,687,10,10,0,0,687,688,5,120,0,0,688,727,3,76,38,11,689,691, + 10,9,0,0,690,692,5,114,0,0,691,690,1,0,0,0,691,692,1,0,0,0,692,693, + 1,0,0,0,693,694,5,16,0,0,694,695,3,76,38,0,695,696,5,6,0,0,696,697, + 3,76,38,10,697,727,1,0,0,0,698,699,10,8,0,0,699,700,5,223,0,0,700, + 701,3,76,38,0,701,702,5,204,0,0,702,703,3,76,38,8,703,727,1,0,0, + 0,704,705,10,19,0,0,705,706,5,216,0,0,706,707,3,76,38,0,707,708, + 5,227,0,0,708,727,1,0,0,0,709,710,10,18,0,0,710,711,5,209,0,0,711, + 727,5,196,0,0,712,713,10,13,0,0,713,715,5,87,0,0,714,716,5,114,0, + 0,715,714,1,0,0,0,715,716,1,0,0,0,716,717,1,0,0,0,717,727,5,115, + 0,0,718,724,10,7,0,0,719,725,3,112,56,0,720,721,5,10,0,0,721,725, + 3,114,57,0,722,723,5,10,0,0,723,725,5,198,0,0,724,719,1,0,0,0,724, + 720,1,0,0,0,724,722,1,0,0,0,725,727,1,0,0,0,726,647,1,0,0,0,726, + 654,1,0,0,0,726,661,1,0,0,0,726,683,1,0,0,0,726,686,1,0,0,0,726, + 689,1,0,0,0,726,698,1,0,0,0,726,704,1,0,0,0,726,709,1,0,0,0,726, + 712,1,0,0,0,726,718,1,0,0,0,727,730,1,0,0,0,728,726,1,0,0,0,728, + 729,1,0,0,0,729,77,1,0,0,0,730,728,1,0,0,0,731,736,3,80,40,0,732, + 733,5,205,0,0,733,735,3,80,40,0,734,732,1,0,0,0,735,738,1,0,0,0, + 736,734,1,0,0,0,736,737,1,0,0,0,737,79,1,0,0,0,738,736,1,0,0,0,739, + 742,3,82,41,0,740,742,3,76,38,0,741,739,1,0,0,0,741,740,1,0,0,0, + 742,81,1,0,0,0,743,744,5,218,0,0,744,749,3,114,57,0,745,746,5,205, + 0,0,746,748,3,114,57,0,747,745,1,0,0,0,748,751,1,0,0,0,749,747,1, + 0,0,0,749,750,1,0,0,0,750,752,1,0,0,0,751,749,1,0,0,0,752,753,5, + 228,0,0,753,763,1,0,0,0,754,759,3,114,57,0,755,756,5,205,0,0,756, + 758,3,114,57,0,757,755,1,0,0,0,758,761,1,0,0,0,759,757,1,0,0,0,759, + 760,1,0,0,0,760,763,1,0,0,0,761,759,1,0,0,0,762,743,1,0,0,0,762, + 754,1,0,0,0,763,764,1,0,0,0,764,765,5,200,0,0,765,766,3,76,38,0, + 766,83,1,0,0,0,767,775,5,199,0,0,768,769,3,92,46,0,769,770,5,209, + 0,0,770,772,1,0,0,0,771,768,1,0,0,0,771,772,1,0,0,0,772,773,1,0, + 0,0,773,775,3,86,43,0,774,767,1,0,0,0,774,771,1,0,0,0,775,85,1,0, + 0,0,776,781,3,114,57,0,777,778,5,209,0,0,778,780,3,114,57,0,779, + 777,1,0,0,0,780,783,1,0,0,0,781,779,1,0,0,0,781,782,1,0,0,0,782, + 87,1,0,0,0,783,781,1,0,0,0,784,785,6,44,-1,0,785,792,3,92,46,0,786, + 792,3,90,45,0,787,788,5,218,0,0,788,789,3,2,1,0,789,790,5,228,0, + 0,790,792,1,0,0,0,791,784,1,0,0,0,791,786,1,0,0,0,791,787,1,0,0, + 0,792,801,1,0,0,0,793,797,10,1,0,0,794,798,3,112,56,0,795,796,5, + 10,0,0,796,798,3,114,57,0,797,794,1,0,0,0,797,795,1,0,0,0,798,800, + 1,0,0,0,799,793,1,0,0,0,800,803,1,0,0,0,801,799,1,0,0,0,801,802, + 1,0,0,0,802,89,1,0,0,0,803,801,1,0,0,0,804,805,3,114,57,0,805,807, + 5,218,0,0,806,808,3,94,47,0,807,806,1,0,0,0,807,808,1,0,0,0,808, + 809,1,0,0,0,809,810,5,228,0,0,810,91,1,0,0,0,811,812,3,98,49,0,812, + 813,5,209,0,0,813,815,1,0,0,0,814,811,1,0,0,0,814,815,1,0,0,0,815, + 816,1,0,0,0,816,817,3,114,57,0,817,93,1,0,0,0,818,823,3,96,48,0, + 819,820,5,205,0,0,820,822,3,96,48,0,821,819,1,0,0,0,822,825,1,0, + 0,0,823,821,1,0,0,0,823,824,1,0,0,0,824,95,1,0,0,0,825,823,1,0,0, + 0,826,830,3,86,43,0,827,830,3,90,45,0,828,830,3,104,52,0,829,826, + 1,0,0,0,829,827,1,0,0,0,829,828,1,0,0,0,830,97,1,0,0,0,831,832,3, + 114,57,0,832,99,1,0,0,0,833,842,5,194,0,0,834,835,5,209,0,0,835, + 842,7,13,0,0,836,837,5,196,0,0,837,839,5,209,0,0,838,840,7,13,0, + 0,839,838,1,0,0,0,839,840,1,0,0,0,840,842,1,0,0,0,841,833,1,0,0, + 0,841,834,1,0,0,0,841,836,1,0,0,0,842,101,1,0,0,0,843,845,7,14,0, + 0,844,843,1,0,0,0,844,845,1,0,0,0,845,852,1,0,0,0,846,853,3,100, + 50,0,847,853,5,195,0,0,848,853,5,196,0,0,849,853,5,197,0,0,850,853, + 5,81,0,0,851,853,5,112,0,0,852,846,1,0,0,0,852,847,1,0,0,0,852,848, + 1,0,0,0,852,849,1,0,0,0,852,850,1,0,0,0,852,851,1,0,0,0,853,103, + 1,0,0,0,854,858,3,102,51,0,855,858,5,198,0,0,856,858,5,115,0,0,857, + 854,1,0,0,0,857,855,1,0,0,0,857,856,1,0,0,0,858,105,1,0,0,0,859, + 860,7,15,0,0,860,107,1,0,0,0,861,862,7,16,0,0,862,109,1,0,0,0,863, + 864,7,17,0,0,864,111,1,0,0,0,865,868,5,193,0,0,866,868,3,110,55, + 0,867,865,1,0,0,0,867,866,1,0,0,0,868,113,1,0,0,0,869,873,5,193, + 0,0,870,873,3,106,53,0,871,873,3,108,54,0,872,869,1,0,0,0,872,870, + 1,0,0,0,872,871,1,0,0,0,873,115,1,0,0,0,874,877,3,114,57,0,875,877, + 5,115,0,0,876,874,1,0,0,0,876,875,1,0,0,0,877,117,1,0,0,0,878,879, + 5,198,0,0,879,880,5,211,0,0,880,881,3,102,51,0,881,119,1,0,0,0,114, + 122,132,140,143,147,150,154,157,160,163,166,169,173,177,180,183, + 186,189,198,204,231,250,258,261,267,275,278,284,286,290,295,298, + 301,305,309,312,314,317,321,325,328,330,332,335,340,351,357,362, + 369,374,378,382,387,394,402,405,408,427,441,457,469,481,489,493, + 500,506,514,519,528,532,563,580,592,602,605,609,612,624,641,645, + 651,658,670,673,677,680,691,715,724,726,728,736,741,749,759,762, + 771,774,781,791,797,801,807,814,823,829,839,841,844,852,857,867, + 872,876 ] class HogQLParser ( Parser ): @@ -488,68 +485,66 @@ class HogQLParser ( Parser ): RULE_havingClause = 12 RULE_orderByClause = 13 RULE_projectionOrderByClause = 14 - RULE_limitByClause = 15 - RULE_limitClause = 16 - RULE_settingsClause = 17 - RULE_joinExpr = 18 - RULE_joinOp = 19 - RULE_joinOpCross = 20 - RULE_joinConstraintClause = 21 - RULE_sampleClause = 22 - RULE_limitExpr = 23 - RULE_orderExprList = 24 - RULE_orderExpr = 25 - RULE_ratioExpr = 26 - RULE_settingExprList = 27 - RULE_settingExpr = 28 - RULE_windowExpr = 29 - RULE_winPartitionByClause = 30 - RULE_winOrderByClause = 31 - RULE_winFrameClause = 32 - RULE_winFrameExtend = 33 - RULE_winFrameBound = 34 - RULE_expr = 35 - RULE_columnTypeExpr = 36 - RULE_columnExprList = 37 - RULE_columnsExpr = 38 - RULE_columnExpr = 39 - RULE_columnArgList = 40 - RULE_columnArgExpr = 41 - RULE_columnLambdaExpr = 42 - RULE_columnIdentifier = 43 - RULE_nestedIdentifier = 44 - RULE_tableExpr = 45 - RULE_tableFunctionExpr = 46 - RULE_tableIdentifier = 47 - RULE_tableArgList = 48 - RULE_tableArgExpr = 49 - RULE_databaseIdentifier = 50 - RULE_floatingLiteral = 51 - RULE_numberLiteral = 52 - RULE_literal = 53 - RULE_interval = 54 - RULE_keyword = 55 - RULE_keywordForAlias = 56 - RULE_alias = 57 - RULE_identifier = 58 - RULE_identifierOrNull = 59 - RULE_enumValue = 60 + RULE_limitClause = 15 + RULE_settingsClause = 16 + RULE_joinExpr = 17 + RULE_joinOp = 18 + RULE_joinOpCross = 19 + RULE_joinConstraintClause = 20 + RULE_sampleClause = 21 + RULE_limitExpr = 22 + RULE_orderExprList = 23 + RULE_orderExpr = 24 + RULE_ratioExpr = 25 + RULE_settingExprList = 26 + RULE_settingExpr = 27 + RULE_windowExpr = 28 + RULE_winPartitionByClause = 29 + RULE_winOrderByClause = 30 + RULE_winFrameClause = 31 + RULE_winFrameExtend = 32 + RULE_winFrameBound = 33 + RULE_expr = 34 + RULE_columnTypeExpr = 35 + RULE_columnExprList = 36 + RULE_columnsExpr = 37 + RULE_columnExpr = 38 + RULE_columnArgList = 39 + RULE_columnArgExpr = 40 + RULE_columnLambdaExpr = 41 + RULE_columnIdentifier = 42 + RULE_nestedIdentifier = 43 + RULE_tableExpr = 44 + RULE_tableFunctionExpr = 45 + RULE_tableIdentifier = 46 + RULE_tableArgList = 47 + RULE_tableArgExpr = 48 + RULE_databaseIdentifier = 49 + RULE_floatingLiteral = 50 + RULE_numberLiteral = 51 + RULE_literal = 52 + RULE_interval = 53 + RULE_keyword = 54 + RULE_keywordForAlias = 55 + RULE_alias = 56 + RULE_identifier = 57 + RULE_identifierOrNull = 58 + RULE_enumValue = 59 ruleNames = [ "select", "selectUnionStmt", "selectStmtWithParens", "selectStmt", "withClause", "topClause", "fromClause", "arrayJoinClause", "windowClause", "prewhereClause", "whereClause", "groupByClause", "havingClause", "orderByClause", - "projectionOrderByClause", "limitByClause", "limitClause", - "settingsClause", "joinExpr", "joinOp", "joinOpCross", - "joinConstraintClause", "sampleClause", "limitExpr", - "orderExprList", "orderExpr", "ratioExpr", "settingExprList", - "settingExpr", "windowExpr", "winPartitionByClause", - "winOrderByClause", "winFrameClause", "winFrameExtend", - "winFrameBound", "expr", "columnTypeExpr", "columnExprList", - "columnsExpr", "columnExpr", "columnArgList", "columnArgExpr", - "columnLambdaExpr", "columnIdentifier", "nestedIdentifier", - "tableExpr", "tableFunctionExpr", "tableIdentifier", - "tableArgList", "tableArgExpr", "databaseIdentifier", + "projectionOrderByClause", "limitClause", "settingsClause", + "joinExpr", "joinOp", "joinOpCross", "joinConstraintClause", + "sampleClause", "limitExpr", "orderExprList", "orderExpr", + "ratioExpr", "settingExprList", "settingExpr", "windowExpr", + "winPartitionByClause", "winOrderByClause", "winFrameClause", + "winFrameExtend", "winFrameBound", "expr", "columnTypeExpr", + "columnExprList", "columnsExpr", "columnExpr", "columnArgList", + "columnArgExpr", "columnLambdaExpr", "columnIdentifier", + "nestedIdentifier", "tableExpr", "tableFunctionExpr", + "tableIdentifier", "tableArgList", "tableArgExpr", "databaseIdentifier", "floatingLiteral", "numberLiteral", "literal", "interval", "keyword", "keywordForAlias", "alias", "identifier", "identifierOrNull", "enumValue" ] @@ -835,21 +830,21 @@ def select(self): self.enterRule(localctx, 0, self.RULE_select) try: self.enterOuterAlt(localctx, 1) - self.state = 124 + self.state = 122 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,0,self._ctx) if la_ == 1: - self.state = 122 + self.state = 120 self.selectUnionStmt() pass elif la_ == 2: - self.state = 123 + self.state = 121 self.selectStmt() pass - self.state = 126 + self.state = 124 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -905,19 +900,19 @@ def selectUnionStmt(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 128 + self.state = 126 self.selectStmtWithParens() - self.state = 134 + self.state = 132 self._errHandler.sync(self) _la = self._input.LA(1) while _la==175: - self.state = 129 + self.state = 127 self.match(HogQLParser.UNION) - self.state = 130 + self.state = 128 self.match(HogQLParser.ALL) - self.state = 131 + self.state = 129 self.selectStmtWithParens() - self.state = 136 + self.state = 134 self._errHandler.sync(self) _la = self._input.LA(1) @@ -968,21 +963,21 @@ def selectStmtWithParens(self): localctx = HogQLParser.SelectStmtWithParensContext(self, self._ctx, self.state) self.enterRule(localctx, 4, self.RULE_selectStmtWithParens) try: - self.state = 142 + self.state = 140 self._errHandler.sync(self) token = self._input.LA(1) if token in [145, 188]: self.enterOuterAlt(localctx, 1) - self.state = 137 + self.state = 135 self.selectStmt() pass elif token in [218]: self.enterOuterAlt(localctx, 2) - self.state = 138 + self.state = 136 self.match(HogQLParser.LPAREN) - self.state = 139 + self.state = 137 self.selectUnionStmt() - self.state = 140 + self.state = 138 self.match(HogQLParser.RPAREN) pass else: @@ -1055,10 +1050,6 @@ def orderByClause(self): return self.getTypedRuleContext(HogQLParser.OrderByClauseContext,0) - def limitByClause(self): - return self.getTypedRuleContext(HogQLParser.LimitByClauseContext,0) - - def limitClause(self): return self.getTypedRuleContext(HogQLParser.LimitClauseContext,0) @@ -1104,89 +1095,89 @@ def selectStmt(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 145 + self.state = 143 self._errHandler.sync(self) _la = self._input.LA(1) if _la==188: - self.state = 144 + self.state = 142 localctx.with_ = self.withClause() - self.state = 147 + self.state = 145 self.match(HogQLParser.SELECT) - self.state = 149 + self.state = 147 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,4,self._ctx) if la_ == 1: - self.state = 148 + self.state = 146 self.match(HogQLParser.DISTINCT) - self.state = 152 + self.state = 150 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,5,self._ctx) if la_ == 1: - self.state = 151 + self.state = 149 self.topClause() - self.state = 154 + self.state = 152 localctx.columns = self.columnExprList() - self.state = 156 + self.state = 154 self._errHandler.sync(self) _la = self._input.LA(1) if _la==67: - self.state = 155 + self.state = 153 localctx.from_ = self.fromClause() - self.state = 159 + self.state = 157 self._errHandler.sync(self) _la = self._input.LA(1) if _la==9 or _la==83 or _la==95: - self.state = 158 + self.state = 156 self.arrayJoinClause() - self.state = 162 + self.state = 160 self._errHandler.sync(self) _la = self._input.LA(1) if _la==187: - self.state = 161 + self.state = 159 self.windowClause() - self.state = 165 + self.state = 163 self._errHandler.sync(self) _la = self._input.LA(1) if _la==128: - self.state = 164 + self.state = 162 self.prewhereClause() - self.state = 168 + self.state = 166 self._errHandler.sync(self) _la = self._input.LA(1) if _la==186: - self.state = 167 + self.state = 165 localctx.where = self.whereClause() - self.state = 171 + self.state = 169 self._errHandler.sync(self) _la = self._input.LA(1) if _la==72: - self.state = 170 + self.state = 168 self.groupByClause() - self.state = 175 + self.state = 173 self._errHandler.sync(self) la_ = self._interp.adaptivePredict(self._input,12,self._ctx) if la_ == 1: - self.state = 173 + self.state = 171 self.match(HogQLParser.WITH) - self.state = 174 + self.state = 172 _la = self._input.LA(1) if not(_la==31 or _la==140): self._errHandler.recoverInline(self) @@ -1195,53 +1186,45 @@ def selectStmt(self): self.consume() - self.state = 179 + self.state = 177 self._errHandler.sync(self) _la = self._input.LA(1) if _la==188: - self.state = 177 + self.state = 175 self.match(HogQLParser.WITH) - self.state = 178 + self.state = 176 self.match(HogQLParser.TOTALS) - self.state = 182 + self.state = 180 self._errHandler.sync(self) _la = self._input.LA(1) if _la==73: - self.state = 181 + self.state = 179 self.havingClause() - self.state = 185 + self.state = 183 self._errHandler.sync(self) _la = self._input.LA(1) if _la==121: - self.state = 184 + self.state = 182 self.orderByClause() - self.state = 188 - self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,16,self._ctx) - if la_ == 1: - self.state = 187 - self.limitByClause() - - - self.state = 191 + self.state = 186 self._errHandler.sync(self) _la = self._input.LA(1) if _la==98: - self.state = 190 + self.state = 185 self.limitClause() - self.state = 194 + self.state = 189 self._errHandler.sync(self) _la = self._input.LA(1) if _la==149: - self.state = 193 + self.state = 188 self.settingsClause() @@ -1286,9 +1269,9 @@ def withClause(self): self.enterRule(localctx, 8, self.RULE_withClause) try: self.enterOuterAlt(localctx, 1) - self.state = 196 + self.state = 191 self.match(HogQLParser.WITH) - self.state = 197 + self.state = 192 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -1336,17 +1319,17 @@ def topClause(self): self.enterRule(localctx, 10, self.RULE_topClause) try: self.enterOuterAlt(localctx, 1) - self.state = 199 + self.state = 194 self.match(HogQLParser.TOP) - self.state = 200 + self.state = 195 self.match(HogQLParser.DECIMAL_LITERAL) - self.state = 203 + self.state = 198 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,19,self._ctx) + la_ = self._interp.adaptivePredict(self._input,18,self._ctx) if la_ == 1: - self.state = 201 + self.state = 196 self.match(HogQLParser.WITH) - self.state = 202 + self.state = 197 self.match(HogQLParser.TIES) @@ -1391,9 +1374,9 @@ def fromClause(self): self.enterRule(localctx, 12, self.RULE_fromClause) try: self.enterOuterAlt(localctx, 1) - self.state = 205 + self.state = 200 self.match(HogQLParser.FROM) - self.state = 206 + self.state = 201 self.joinExpr(0) except RecognitionException as re: localctx.exception = re @@ -1446,11 +1429,11 @@ def arrayJoinClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 209 + self.state = 204 self._errHandler.sync(self) _la = self._input.LA(1) if _la==83 or _la==95: - self.state = 208 + self.state = 203 _la = self._input.LA(1) if not(_la==83 or _la==95): self._errHandler.recoverInline(self) @@ -1459,11 +1442,11 @@ def arrayJoinClause(self): self.consume() - self.state = 211 + self.state = 206 self.match(HogQLParser.ARRAY) - self.state = 212 + self.state = 207 self.match(HogQLParser.JOIN) - self.state = 213 + self.state = 208 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -1519,17 +1502,17 @@ def windowClause(self): self.enterRule(localctx, 16, self.RULE_windowClause) try: self.enterOuterAlt(localctx, 1) - self.state = 215 + self.state = 210 self.match(HogQLParser.WINDOW) - self.state = 216 + self.state = 211 self.identifier() - self.state = 217 + self.state = 212 self.match(HogQLParser.AS) - self.state = 218 + self.state = 213 self.match(HogQLParser.LPAREN) - self.state = 219 + self.state = 214 self.windowExpr() - self.state = 220 + self.state = 215 self.match(HogQLParser.RPAREN) except RecognitionException as re: localctx.exception = re @@ -1572,9 +1555,9 @@ def prewhereClause(self): self.enterRule(localctx, 18, self.RULE_prewhereClause) try: self.enterOuterAlt(localctx, 1) - self.state = 222 + self.state = 217 self.match(HogQLParser.PREWHERE) - self.state = 223 + self.state = 218 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -1617,9 +1600,9 @@ def whereClause(self): self.enterRule(localctx, 20, self.RULE_whereClause) try: self.enterOuterAlt(localctx, 1) - self.state = 225 + self.state = 220 self.match(HogQLParser.WHERE) - self.state = 226 + self.state = 221 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -1678,31 +1661,31 @@ def groupByClause(self): self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 228 + self.state = 223 self.match(HogQLParser.GROUP) - self.state = 229 + self.state = 224 self.match(HogQLParser.BY) - self.state = 236 + self.state = 231 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,21,self._ctx) + la_ = self._interp.adaptivePredict(self._input,20,self._ctx) if la_ == 1: - self.state = 230 + self.state = 225 _la = self._input.LA(1) if not(_la==31 or _la==140): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 231 + self.state = 226 self.match(HogQLParser.LPAREN) - self.state = 232 + self.state = 227 self.columnExprList() - self.state = 233 + self.state = 228 self.match(HogQLParser.RPAREN) pass elif la_ == 2: - self.state = 235 + self.state = 230 self.columnExprList() pass @@ -1748,9 +1731,9 @@ def havingClause(self): self.enterRule(localctx, 24, self.RULE_havingClause) try: self.enterOuterAlt(localctx, 1) - self.state = 238 + self.state = 233 self.match(HogQLParser.HAVING) - self.state = 239 + self.state = 234 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -1796,11 +1779,11 @@ def orderByClause(self): self.enterRule(localctx, 26, self.RULE_orderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 241 + self.state = 236 self.match(HogQLParser.ORDER) - self.state = 242 + self.state = 237 self.match(HogQLParser.BY) - self.state = 243 + self.state = 238 self.orderExprList() except RecognitionException as re: localctx.exception = re @@ -1846,11 +1829,11 @@ def projectionOrderByClause(self): self.enterRule(localctx, 28, self.RULE_projectionOrderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 245 + self.state = 240 self.match(HogQLParser.ORDER) - self.state = 246 + self.state = 241 self.match(HogQLParser.BY) - self.state = 247 + self.state = 242 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -1861,7 +1844,7 @@ def projectionOrderByClause(self): return localctx - class LimitByClauseContext(ParserRuleContext): + class LimitClauseContext(ParserRuleContext): __slots__ = 'parser' def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): @@ -1882,55 +1865,6 @@ def columnExprList(self): return self.getTypedRuleContext(HogQLParser.ColumnExprListContext,0) - def getRuleIndex(self): - return HogQLParser.RULE_limitByClause - - def accept(self, visitor:ParseTreeVisitor): - if hasattr( visitor, "visitLimitByClause" ): - return visitor.visitLimitByClause(self) - else: - return visitor.visitChildren(self) - - - - - def limitByClause(self): - - localctx = HogQLParser.LimitByClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 30, self.RULE_limitByClause) - try: - self.enterOuterAlt(localctx, 1) - self.state = 249 - self.match(HogQLParser.LIMIT) - self.state = 250 - self.limitExpr() - self.state = 251 - self.match(HogQLParser.BY) - self.state = 252 - self.columnExprList() - except RecognitionException as re: - localctx.exception = re - self._errHandler.reportError(self, re) - self._errHandler.recover(self, re) - finally: - self.exitRule() - return localctx - - - class LimitClauseContext(ParserRuleContext): - __slots__ = 'parser' - - def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): - super().__init__(parent, invokingState) - self.parser = parser - - def LIMIT(self): - return self.getToken(HogQLParser.LIMIT, 0) - - def limitExpr(self): - return self.getTypedRuleContext(HogQLParser.LimitExprContext,0) - - def WITH(self): return self.getToken(HogQLParser.WITH, 0) @@ -1952,24 +1886,32 @@ def accept(self, visitor:ParseTreeVisitor): def limitClause(self): localctx = HogQLParser.LimitClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 32, self.RULE_limitClause) - self._la = 0 # Token type + self.enterRule(localctx, 30, self.RULE_limitClause) try: self.enterOuterAlt(localctx, 1) - self.state = 254 + self.state = 244 self.match(HogQLParser.LIMIT) - self.state = 255 + self.state = 245 self.limitExpr() - self.state = 258 + self.state = 250 self._errHandler.sync(self) - _la = self._input.LA(1) - if _la==188: - self.state = 256 + token = self._input.LA(1) + if token in [188]: + self.state = 246 self.match(HogQLParser.WITH) - self.state = 257 + self.state = 247 self.match(HogQLParser.TIES) - - + pass + elif token in [18]: + self.state = 248 + self.match(HogQLParser.BY) + self.state = 249 + self.columnExprList() + pass + elif token in [-1, 149, 175, 228]: + pass + else: + pass except RecognitionException as re: localctx.exception = re self._errHandler.reportError(self, re) @@ -2008,12 +1950,12 @@ def accept(self, visitor:ParseTreeVisitor): def settingsClause(self): localctx = HogQLParser.SettingsClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 34, self.RULE_settingsClause) + self.enterRule(localctx, 32, self.RULE_settingsClause) try: self.enterOuterAlt(localctx, 1) - self.state = 260 + self.state = 252 self.match(HogQLParser.SETTINGS) - self.state = 261 + self.state = 253 self.settingExprList() except RecognitionException as re: localctx.exception = re @@ -2144,34 +2086,34 @@ def joinExpr(self, _p:int=0): _parentState = self.state localctx = HogQLParser.JoinExprContext(self, self._ctx, _parentState) _prevctx = localctx - _startState = 36 - self.enterRecursionRule(localctx, 36, self.RULE_joinExpr, _p) + _startState = 34 + self.enterRecursionRule(localctx, 34, self.RULE_joinExpr, _p) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 275 + self.state = 267 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,25,self._ctx) + la_ = self._interp.adaptivePredict(self._input,24,self._ctx) if la_ == 1: localctx = HogQLParser.JoinExprTableContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 264 + self.state = 256 self.tableExpr(0) - self.state = 266 + self.state = 258 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,23,self._ctx) + la_ = self._interp.adaptivePredict(self._input,22,self._ctx) if la_ == 1: - self.state = 265 + self.state = 257 self.match(HogQLParser.FINAL) - self.state = 269 + self.state = 261 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,24,self._ctx) + la_ = self._interp.adaptivePredict(self._input,23,self._ctx) if la_ == 1: - self.state = 268 + self.state = 260 self.sampleClause() @@ -2181,52 +2123,52 @@ def joinExpr(self, _p:int=0): localctx = HogQLParser.JoinExprParensContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 271 + self.state = 263 self.match(HogQLParser.LPAREN) - self.state = 272 + self.state = 264 self.joinExpr(0) - self.state = 273 + self.state = 265 self.match(HogQLParser.RPAREN) pass self._ctx.stop = self._input.LT(-1) - self.state = 294 + self.state = 286 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,29,self._ctx) + _alt = self._interp.adaptivePredict(self._input,28,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 292 + self.state = 284 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,28,self._ctx) + la_ = self._interp.adaptivePredict(self._input,27,self._ctx) if la_ == 1: localctx = HogQLParser.JoinExprCrossOpContext(self, HogQLParser.JoinExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_joinExpr) - self.state = 277 + self.state = 269 if not self.precpred(self._ctx, 3): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 3)") - self.state = 278 + self.state = 270 self.joinOpCross() - self.state = 279 + self.state = 271 self.joinExpr(4) pass elif la_ == 2: localctx = HogQLParser.JoinExprOpContext(self, HogQLParser.JoinExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_joinExpr) - self.state = 281 + self.state = 273 if not self.precpred(self._ctx, 4): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 4)") - self.state = 283 + self.state = 275 self._errHandler.sync(self) _la = self._input.LA(1) if _la==70 or _la==100: - self.state = 282 + self.state = 274 _la = self._input.LA(1) if not(_la==70 or _la==100): self._errHandler.recoverInline(self) @@ -2235,26 +2177,26 @@ def joinExpr(self, _p:int=0): self.consume() - self.state = 286 + self.state = 278 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 4496) != 0 or (((_la - 68)) & ~0x3f) == 0 and ((1 << (_la - 68)) & 134250497) != 0 or _la==139 or _la==146: - self.state = 285 + self.state = 277 self.joinOp() - self.state = 288 + self.state = 280 self.match(HogQLParser.JOIN) - self.state = 289 + self.state = 281 self.joinExpr(0) - self.state = 290 + self.state = 282 self.joinConstraintClause() pass - self.state = 296 + self.state = 288 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,29,self._ctx) + _alt = self._interp.adaptivePredict(self._input,28,self._ctx) except RecognitionException as re: localctx.exception = re @@ -2360,24 +2302,24 @@ def accept(self, visitor:ParseTreeVisitor): def joinOp(self): localctx = HogQLParser.JoinOpContext(self, self._ctx, self.state) - self.enterRule(localctx, 38, self.RULE_joinOp) + self.enterRule(localctx, 36, self.RULE_joinOp) self._la = 0 # Token type try: - self.state = 340 + self.state = 332 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,43,self._ctx) + la_ = self._interp.adaptivePredict(self._input,42,self._ctx) if la_ == 1: localctx = HogQLParser.JoinOpInnerContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 306 + self.state = 298 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,32,self._ctx) + la_ = self._interp.adaptivePredict(self._input,31,self._ctx) if la_ == 1: - self.state = 298 + self.state = 290 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 4368) != 0: - self.state = 297 + self.state = 289 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 4368) != 0): self._errHandler.recoverInline(self) @@ -2386,18 +2328,18 @@ def joinOp(self): self.consume() - self.state = 300 + self.state = 292 self.match(HogQLParser.INNER) pass elif la_ == 2: - self.state = 301 + self.state = 293 self.match(HogQLParser.INNER) - self.state = 303 + self.state = 295 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 4368) != 0: - self.state = 302 + self.state = 294 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 4368) != 0): self._errHandler.recoverInline(self) @@ -2409,7 +2351,7 @@ def joinOp(self): pass elif la_ == 3: - self.state = 305 + self.state = 297 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 4368) != 0): self._errHandler.recoverInline(self) @@ -2424,15 +2366,15 @@ def joinOp(self): elif la_ == 2: localctx = HogQLParser.JoinOpLeftRightContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 322 + self.state = 314 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,37,self._ctx) + la_ = self._interp.adaptivePredict(self._input,36,self._ctx) if la_ == 1: - self.state = 309 + self.state = 301 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 4496) != 0 or _la==146: - self.state = 308 + self.state = 300 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 4496) != 0 or _la==146): self._errHandler.recoverInline(self) @@ -2441,44 +2383,44 @@ def joinOp(self): self.consume() - self.state = 311 + self.state = 303 _la = self._input.LA(1) if not(_la==95 or _la==139): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 313 + self.state = 305 self._errHandler.sync(self) _la = self._input.LA(1) if _la==122: - self.state = 312 + self.state = 304 self.match(HogQLParser.OUTER) pass elif la_ == 2: - self.state = 315 + self.state = 307 _la = self._input.LA(1) if not(_la==95 or _la==139): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 317 + self.state = 309 self._errHandler.sync(self) _la = self._input.LA(1) if _la==122: - self.state = 316 + self.state = 308 self.match(HogQLParser.OUTER) - self.state = 320 + self.state = 312 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 4496) != 0 or _la==146: - self.state = 319 + self.state = 311 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 4496) != 0 or _la==146): self._errHandler.recoverInline(self) @@ -2495,15 +2437,15 @@ def joinOp(self): elif la_ == 3: localctx = HogQLParser.JoinOpFullContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 338 + self.state = 330 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,42,self._ctx) + la_ = self._interp.adaptivePredict(self._input,41,self._ctx) if la_ == 1: - self.state = 325 + self.state = 317 self._errHandler.sync(self) _la = self._input.LA(1) if _la==4 or _la==8: - self.state = 324 + self.state = 316 _la = self._input.LA(1) if not(_la==4 or _la==8): self._errHandler.recoverInline(self) @@ -2512,34 +2454,34 @@ def joinOp(self): self.consume() - self.state = 327 + self.state = 319 self.match(HogQLParser.FULL) - self.state = 329 + self.state = 321 self._errHandler.sync(self) _la = self._input.LA(1) if _la==122: - self.state = 328 + self.state = 320 self.match(HogQLParser.OUTER) pass elif la_ == 2: - self.state = 331 + self.state = 323 self.match(HogQLParser.FULL) - self.state = 333 + self.state = 325 self._errHandler.sync(self) _la = self._input.LA(1) if _la==122: - self.state = 332 + self.state = 324 self.match(HogQLParser.OUTER) - self.state = 336 + self.state = 328 self._errHandler.sync(self) _la = self._input.LA(1) if _la==4 or _la==8: - self.state = 335 + self.state = 327 _la = self._input.LA(1) if not(_la==4 or _la==8): self._errHandler.recoverInline(self) @@ -2600,19 +2542,19 @@ def accept(self, visitor:ParseTreeVisitor): def joinOpCross(self): localctx = HogQLParser.JoinOpCrossContext(self, self._ctx, self.state) - self.enterRule(localctx, 40, self.RULE_joinOpCross) + self.enterRule(localctx, 38, self.RULE_joinOpCross) self._la = 0 # Token type try: - self.state = 348 + self.state = 340 self._errHandler.sync(self) token = self._input.LA(1) if token in [30, 70, 100]: self.enterOuterAlt(localctx, 1) - self.state = 343 + self.state = 335 self._errHandler.sync(self) _la = self._input.LA(1) if _la==70 or _la==100: - self.state = 342 + self.state = 334 _la = self._input.LA(1) if not(_la==70 or _la==100): self._errHandler.recoverInline(self) @@ -2621,14 +2563,14 @@ def joinOpCross(self): self.consume() - self.state = 345 + self.state = 337 self.match(HogQLParser.CROSS) - self.state = 346 + self.state = 338 self.match(HogQLParser.JOIN) pass elif token in [205]: self.enterOuterAlt(localctx, 2) - self.state = 347 + self.state = 339 self.match(HogQLParser.COMMA) pass else: @@ -2681,36 +2623,36 @@ def accept(self, visitor:ParseTreeVisitor): def joinConstraintClause(self): localctx = HogQLParser.JoinConstraintClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 42, self.RULE_joinConstraintClause) + self.enterRule(localctx, 40, self.RULE_joinConstraintClause) try: - self.state = 359 + self.state = 351 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,46,self._ctx) + la_ = self._interp.adaptivePredict(self._input,45,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 350 + self.state = 342 self.match(HogQLParser.ON) - self.state = 351 + self.state = 343 self.columnExprList() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 352 + self.state = 344 self.match(HogQLParser.USING) - self.state = 353 + self.state = 345 self.match(HogQLParser.LPAREN) - self.state = 354 + self.state = 346 self.columnExprList() - self.state = 355 + self.state = 347 self.match(HogQLParser.RPAREN) pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 357 + self.state = 349 self.match(HogQLParser.USING) - self.state = 358 + self.state = 350 self.columnExprList() pass @@ -2759,20 +2701,20 @@ def accept(self, visitor:ParseTreeVisitor): def sampleClause(self): localctx = HogQLParser.SampleClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 44, self.RULE_sampleClause) + self.enterRule(localctx, 42, self.RULE_sampleClause) try: self.enterOuterAlt(localctx, 1) - self.state = 361 + self.state = 353 self.match(HogQLParser.SAMPLE) - self.state = 362 + self.state = 354 self.ratioExpr() - self.state = 365 + self.state = 357 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,47,self._ctx) + la_ = self._interp.adaptivePredict(self._input,46,self._ctx) if la_ == 1: - self.state = 363 + self.state = 355 self.match(HogQLParser.OFFSET) - self.state = 364 + self.state = 356 self.ratioExpr() @@ -2820,24 +2762,24 @@ def accept(self, visitor:ParseTreeVisitor): def limitExpr(self): localctx = HogQLParser.LimitExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 46, self.RULE_limitExpr) + self.enterRule(localctx, 44, self.RULE_limitExpr) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 367 + self.state = 359 self.columnExpr(0) - self.state = 370 + self.state = 362 self._errHandler.sync(self) _la = self._input.LA(1) if _la==117 or _la==205: - self.state = 368 + self.state = 360 _la = self._input.LA(1) if not(_la==117 or _la==205): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 369 + self.state = 361 self.columnExpr(0) @@ -2885,21 +2827,21 @@ def accept(self, visitor:ParseTreeVisitor): def orderExprList(self): localctx = HogQLParser.OrderExprListContext(self, self._ctx, self.state) - self.enterRule(localctx, 48, self.RULE_orderExprList) + self.enterRule(localctx, 46, self.RULE_orderExprList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 372 + self.state = 364 self.orderExpr() - self.state = 377 + self.state = 369 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 373 + self.state = 365 self.match(HogQLParser.COMMA) - self.state = 374 + self.state = 366 self.orderExpr() - self.state = 379 + self.state = 371 self._errHandler.sync(self) _la = self._input.LA(1) @@ -2962,17 +2904,17 @@ def accept(self, visitor:ParseTreeVisitor): def orderExpr(self): localctx = HogQLParser.OrderExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 50, self.RULE_orderExpr) + self.enterRule(localctx, 48, self.RULE_orderExpr) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 380 + self.state = 372 self.columnExpr(0) - self.state = 382 + self.state = 374 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & 6597069768704) != 0: - self.state = 381 + self.state = 373 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & 6597069768704) != 0): self._errHandler.recoverInline(self) @@ -2981,13 +2923,13 @@ def orderExpr(self): self.consume() - self.state = 386 + self.state = 378 self._errHandler.sync(self) _la = self._input.LA(1) if _la==116: - self.state = 384 + self.state = 376 self.match(HogQLParser.NULLS) - self.state = 385 + self.state = 377 _la = self._input.LA(1) if not(_la==61 or _la==92): self._errHandler.recoverInline(self) @@ -2996,13 +2938,13 @@ def orderExpr(self): self.consume() - self.state = 390 + self.state = 382 self._errHandler.sync(self) _la = self._input.LA(1) if _la==25: - self.state = 388 + self.state = 380 self.match(HogQLParser.COLLATE) - self.state = 389 + self.state = 381 self.match(HogQLParser.STRING_LITERAL) @@ -3047,18 +2989,18 @@ def accept(self, visitor:ParseTreeVisitor): def ratioExpr(self): localctx = HogQLParser.RatioExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 52, self.RULE_ratioExpr) + self.enterRule(localctx, 50, self.RULE_ratioExpr) try: self.enterOuterAlt(localctx, 1) - self.state = 392 + self.state = 384 self.numberLiteral() - self.state = 395 + self.state = 387 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,53,self._ctx) + la_ = self._interp.adaptivePredict(self._input,52,self._ctx) if la_ == 1: - self.state = 393 + self.state = 385 self.match(HogQLParser.SLASH) - self.state = 394 + self.state = 386 self.numberLiteral() @@ -3106,21 +3048,21 @@ def accept(self, visitor:ParseTreeVisitor): def settingExprList(self): localctx = HogQLParser.SettingExprListContext(self, self._ctx, self.state) - self.enterRule(localctx, 54, self.RULE_settingExprList) + self.enterRule(localctx, 52, self.RULE_settingExprList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 397 + self.state = 389 self.settingExpr() - self.state = 402 + self.state = 394 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 398 + self.state = 390 self.match(HogQLParser.COMMA) - self.state = 399 + self.state = 391 self.settingExpr() - self.state = 404 + self.state = 396 self._errHandler.sync(self) _la = self._input.LA(1) @@ -3166,14 +3108,14 @@ def accept(self, visitor:ParseTreeVisitor): def settingExpr(self): localctx = HogQLParser.SettingExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 56, self.RULE_settingExpr) + self.enterRule(localctx, 54, self.RULE_settingExpr) try: self.enterOuterAlt(localctx, 1) - self.state = 405 + self.state = 397 self.identifier() - self.state = 406 + self.state = 398 self.match(HogQLParser.EQ_SINGLE) - self.state = 407 + self.state = 399 self.literal() except RecognitionException as re: localctx.exception = re @@ -3218,31 +3160,31 @@ def accept(self, visitor:ParseTreeVisitor): def windowExpr(self): localctx = HogQLParser.WindowExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 58, self.RULE_windowExpr) + self.enterRule(localctx, 56, self.RULE_windowExpr) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 410 + self.state = 402 self._errHandler.sync(self) _la = self._input.LA(1) if _la==125: - self.state = 409 + self.state = 401 self.winPartitionByClause() - self.state = 413 + self.state = 405 self._errHandler.sync(self) _la = self._input.LA(1) if _la==121: - self.state = 412 + self.state = 404 self.winOrderByClause() - self.state = 416 + self.state = 408 self._errHandler.sync(self) _la = self._input.LA(1) if _la==132 or _la==142: - self.state = 415 + self.state = 407 self.winFrameClause() @@ -3287,14 +3229,14 @@ def accept(self, visitor:ParseTreeVisitor): def winPartitionByClause(self): localctx = HogQLParser.WinPartitionByClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 60, self.RULE_winPartitionByClause) + self.enterRule(localctx, 58, self.RULE_winPartitionByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 418 + self.state = 410 self.match(HogQLParser.PARTITION) - self.state = 419 + self.state = 411 self.match(HogQLParser.BY) - self.state = 420 + self.state = 412 self.columnExprList() except RecognitionException as re: localctx.exception = re @@ -3337,14 +3279,14 @@ def accept(self, visitor:ParseTreeVisitor): def winOrderByClause(self): localctx = HogQLParser.WinOrderByClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 62, self.RULE_winOrderByClause) + self.enterRule(localctx, 60, self.RULE_winOrderByClause) try: self.enterOuterAlt(localctx, 1) - self.state = 422 + self.state = 414 self.match(HogQLParser.ORDER) - self.state = 423 + self.state = 415 self.match(HogQLParser.BY) - self.state = 424 + self.state = 416 self.orderExprList() except RecognitionException as re: localctx.exception = re @@ -3387,18 +3329,18 @@ def accept(self, visitor:ParseTreeVisitor): def winFrameClause(self): localctx = HogQLParser.WinFrameClauseContext(self, self._ctx, self.state) - self.enterRule(localctx, 64, self.RULE_winFrameClause) + self.enterRule(localctx, 62, self.RULE_winFrameClause) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 426 + self.state = 418 _la = self._input.LA(1) if not(_la==132 or _la==142): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 427 + self.state = 419 self.winFrameExtend() except RecognitionException as re: localctx.exception = re @@ -3471,27 +3413,27 @@ def accept(self, visitor:ParseTreeVisitor): def winFrameExtend(self): localctx = HogQLParser.WinFrameExtendContext(self, self._ctx, self.state) - self.enterRule(localctx, 66, self.RULE_winFrameExtend) + self.enterRule(localctx, 64, self.RULE_winFrameExtend) try: - self.state = 435 + self.state = 427 self._errHandler.sync(self) token = self._input.LA(1) if token in [32, 81, 112, 174, 194, 195, 196, 197, 207, 209, 222]: localctx = HogQLParser.FrameStartContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 429 + self.state = 421 self.winFrameBound() pass elif token in [16]: localctx = HogQLParser.FrameBetweenContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 430 + self.state = 422 self.match(HogQLParser.BETWEEN) - self.state = 431 + self.state = 423 self.winFrameBound() - self.state = 432 + self.state = 424 self.match(HogQLParser.AND) - self.state = 433 + self.state = 425 self.winFrameBound() pass else: @@ -3547,44 +3489,44 @@ def accept(self, visitor:ParseTreeVisitor): def winFrameBound(self): localctx = HogQLParser.WinFrameBoundContext(self, self._ctx, self.state) - self.enterRule(localctx, 68, self.RULE_winFrameBound) + self.enterRule(localctx, 66, self.RULE_winFrameBound) try: self.enterOuterAlt(localctx, 1) - self.state = 449 + self.state = 441 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,59,self._ctx) + la_ = self._interp.adaptivePredict(self._input,58,self._ctx) if la_ == 1: - self.state = 437 + self.state = 429 self.match(HogQLParser.CURRENT) - self.state = 438 + self.state = 430 self.match(HogQLParser.ROW) pass elif la_ == 2: - self.state = 439 + self.state = 431 self.match(HogQLParser.UNBOUNDED) - self.state = 440 + self.state = 432 self.match(HogQLParser.PRECEDING) pass elif la_ == 3: - self.state = 441 + self.state = 433 self.match(HogQLParser.UNBOUNDED) - self.state = 442 + self.state = 434 self.match(HogQLParser.FOLLOWING) pass elif la_ == 4: - self.state = 443 + self.state = 435 self.numberLiteral() - self.state = 444 + self.state = 436 self.match(HogQLParser.PRECEDING) pass elif la_ == 5: - self.state = 446 + self.state = 438 self.numberLiteral() - self.state = 447 + self.state = 439 self.match(HogQLParser.FOLLOWING) pass @@ -3627,12 +3569,12 @@ def accept(self, visitor:ParseTreeVisitor): def expr(self): localctx = HogQLParser.ExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 70, self.RULE_expr) + self.enterRule(localctx, 68, self.RULE_expr) try: self.enterOuterAlt(localctx, 1) - self.state = 451 + self.state = 443 self.columnExpr(0) - self.state = 452 + self.state = 444 self.match(HogQLParser.EOF) except RecognitionException as re: localctx.exception = re @@ -3804,114 +3746,114 @@ def accept(self, visitor:ParseTreeVisitor): def columnTypeExpr(self): localctx = HogQLParser.ColumnTypeExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 72, self.RULE_columnTypeExpr) + self.enterRule(localctx, 70, self.RULE_columnTypeExpr) self._la = 0 # Token type try: - self.state = 501 + self.state = 493 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,64,self._ctx) + la_ = self._interp.adaptivePredict(self._input,63,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnTypeExprSimpleContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 454 + self.state = 446 self.identifier() pass elif la_ == 2: localctx = HogQLParser.ColumnTypeExprNestedContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 455 + self.state = 447 self.identifier() - self.state = 456 + self.state = 448 self.match(HogQLParser.LPAREN) - self.state = 457 + self.state = 449 self.identifier() - self.state = 458 + self.state = 450 self.columnTypeExpr() - self.state = 465 + self.state = 457 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 459 + self.state = 451 self.match(HogQLParser.COMMA) - self.state = 460 + self.state = 452 self.identifier() - self.state = 461 + self.state = 453 self.columnTypeExpr() - self.state = 467 + self.state = 459 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 468 + self.state = 460 self.match(HogQLParser.RPAREN) pass elif la_ == 3: localctx = HogQLParser.ColumnTypeExprEnumContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 470 + self.state = 462 self.identifier() - self.state = 471 + self.state = 463 self.match(HogQLParser.LPAREN) - self.state = 472 + self.state = 464 self.enumValue() - self.state = 477 + self.state = 469 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 473 + self.state = 465 self.match(HogQLParser.COMMA) - self.state = 474 + self.state = 466 self.enumValue() - self.state = 479 + self.state = 471 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 480 + self.state = 472 self.match(HogQLParser.RPAREN) pass elif la_ == 4: localctx = HogQLParser.ColumnTypeExprComplexContext(self, localctx) self.enterOuterAlt(localctx, 4) - self.state = 482 + self.state = 474 self.identifier() - self.state = 483 + self.state = 475 self.match(HogQLParser.LPAREN) - self.state = 484 + self.state = 476 self.columnTypeExpr() - self.state = 489 + self.state = 481 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 485 + self.state = 477 self.match(HogQLParser.COMMA) - self.state = 486 + self.state = 478 self.columnTypeExpr() - self.state = 491 + self.state = 483 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 492 + self.state = 484 self.match(HogQLParser.RPAREN) pass elif la_ == 5: localctx = HogQLParser.ColumnTypeExprParamContext(self, localctx) self.enterOuterAlt(localctx, 5) - self.state = 494 + self.state = 486 self.identifier() - self.state = 495 + self.state = 487 self.match(HogQLParser.LPAREN) - self.state = 497 + self.state = 489 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 496 + self.state = 488 self.columnExprList() - self.state = 499 + self.state = 491 self.match(HogQLParser.RPAREN) pass @@ -3960,23 +3902,23 @@ def accept(self, visitor:ParseTreeVisitor): def columnExprList(self): localctx = HogQLParser.ColumnExprListContext(self, self._ctx, self.state) - self.enterRule(localctx, 74, self.RULE_columnExprList) + self.enterRule(localctx, 72, self.RULE_columnExprList) try: self.enterOuterAlt(localctx, 1) - self.state = 503 + self.state = 495 self.columnsExpr() - self.state = 508 + self.state = 500 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,65,self._ctx) + _alt = self._interp.adaptivePredict(self._input,64,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: - self.state = 504 + self.state = 496 self.match(HogQLParser.COMMA) - self.state = 505 + self.state = 497 self.columnsExpr() - self.state = 510 + self.state = 502 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,65,self._ctx) + _alt = self._interp.adaptivePredict(self._input,64,self._ctx) except RecognitionException as re: localctx.exception = re @@ -4067,44 +4009,44 @@ def accept(self, visitor:ParseTreeVisitor): def columnsExpr(self): localctx = HogQLParser.ColumnsExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 76, self.RULE_columnsExpr) + self.enterRule(localctx, 74, self.RULE_columnsExpr) self._la = 0 # Token type try: - self.state = 522 + self.state = 514 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,67,self._ctx) + la_ = self._interp.adaptivePredict(self._input,66,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnsExprAsteriskContext(self, localctx) self.enterOuterAlt(localctx, 1) - self.state = 514 + self.state = 506 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la - 2)) & ~0x3f) == 0 and ((1 << (_la - 2)) & -1) != 0 or (((_la - 66)) & ~0x3f) == 0 and ((1 << (_la - 66)) & -633318697631745) != 0 or (((_la - 131)) & ~0x3f) == 0 and ((1 << (_la - 131)) & 6917529027641081855) != 0: - self.state = 511 + self.state = 503 self.tableIdentifier() - self.state = 512 + self.state = 504 self.match(HogQLParser.DOT) - self.state = 516 + self.state = 508 self.match(HogQLParser.ASTERISK) pass elif la_ == 2: localctx = HogQLParser.ColumnsExprSubqueryContext(self, localctx) self.enterOuterAlt(localctx, 2) - self.state = 517 + self.state = 509 self.match(HogQLParser.LPAREN) - self.state = 518 + self.state = 510 self.selectUnionStmt() - self.state = 519 + self.state = 511 self.match(HogQLParser.RPAREN) pass elif la_ == 3: localctx = HogQLParser.ColumnsExprColumnContext(self, localctx) self.enterOuterAlt(localctx, 3) - self.state = 521 + self.state = 513 self.columnExpr(0) pass @@ -4927,58 +4869,58 @@ def columnExpr(self, _p:int=0): _parentState = self.state localctx = HogQLParser.ColumnExprContext(self, self._ctx, _parentState) _prevctx = localctx - _startState = 78 - self.enterRecursionRule(localctx, 78, self.RULE_columnExpr, _p) + _startState = 76 + self.enterRecursionRule(localctx, 76, self.RULE_columnExpr, _p) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 653 + self.state = 645 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,80,self._ctx) + la_ = self._interp.adaptivePredict(self._input,79,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprCaseContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 525 + self.state = 517 self.match(HogQLParser.CASE) - self.state = 527 + self.state = 519 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,68,self._ctx) + la_ = self._interp.adaptivePredict(self._input,67,self._ctx) if la_ == 1: - self.state = 526 + self.state = 518 localctx.caseExpr = self.columnExpr(0) - self.state = 534 + self.state = 526 self._errHandler.sync(self) _la = self._input.LA(1) while True: - self.state = 529 + self.state = 521 self.match(HogQLParser.WHEN) - self.state = 530 + self.state = 522 localctx.whenExpr = self.columnExpr(0) - self.state = 531 + self.state = 523 self.match(HogQLParser.THEN) - self.state = 532 + self.state = 524 localctx.thenExpr = self.columnExpr(0) - self.state = 536 + self.state = 528 self._errHandler.sync(self) _la = self._input.LA(1) if not (_la==185): break - self.state = 540 + self.state = 532 self._errHandler.sync(self) _la = self._input.LA(1) if _la==51: - self.state = 538 + self.state = 530 self.match(HogQLParser.ELSE) - self.state = 539 + self.state = 531 localctx.elseExpr = self.columnExpr(0) - self.state = 542 + self.state = 534 self.match(HogQLParser.END) pass @@ -4986,17 +4928,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprCastContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 544 + self.state = 536 self.match(HogQLParser.CAST) - self.state = 545 + self.state = 537 self.match(HogQLParser.LPAREN) - self.state = 546 + self.state = 538 self.columnExpr(0) - self.state = 547 + self.state = 539 self.match(HogQLParser.AS) - self.state = 548 + self.state = 540 self.columnTypeExpr() - self.state = 549 + self.state = 541 self.match(HogQLParser.RPAREN) pass @@ -5004,9 +4946,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprDateContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 551 + self.state = 543 self.match(HogQLParser.DATE) - self.state = 552 + self.state = 544 self.match(HogQLParser.STRING_LITERAL) pass @@ -5014,17 +4956,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprExtractContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 553 + self.state = 545 self.match(HogQLParser.EXTRACT) - self.state = 554 + self.state = 546 self.match(HogQLParser.LPAREN) - self.state = 555 + self.state = 547 self.interval() - self.state = 556 + self.state = 548 self.match(HogQLParser.FROM) - self.state = 557 + self.state = 549 self.columnExpr(0) - self.state = 558 + self.state = 550 self.match(HogQLParser.RPAREN) pass @@ -5032,11 +4974,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprIntervalContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 560 + self.state = 552 self.match(HogQLParser.INTERVAL) - self.state = 561 + self.state = 553 self.columnExpr(0) - self.state = 562 + self.state = 554 self.interval() pass @@ -5044,27 +4986,27 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprSubstringContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 564 + self.state = 556 self.match(HogQLParser.SUBSTRING) - self.state = 565 + self.state = 557 self.match(HogQLParser.LPAREN) - self.state = 566 + self.state = 558 self.columnExpr(0) - self.state = 567 + self.state = 559 self.match(HogQLParser.FROM) - self.state = 568 + self.state = 560 self.columnExpr(0) - self.state = 571 + self.state = 563 self._errHandler.sync(self) _la = self._input.LA(1) if _la==64: - self.state = 569 + self.state = 561 self.match(HogQLParser.FOR) - self.state = 570 + self.state = 562 self.columnExpr(0) - self.state = 573 + self.state = 565 self.match(HogQLParser.RPAREN) pass @@ -5072,9 +5014,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTimestampContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 575 + self.state = 567 self.match(HogQLParser.TIMESTAMP) - self.state = 576 + self.state = 568 self.match(HogQLParser.STRING_LITERAL) pass @@ -5082,24 +5024,24 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTrimContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 577 + self.state = 569 self.match(HogQLParser.TRIM) - self.state = 578 + self.state = 570 self.match(HogQLParser.LPAREN) - self.state = 579 + self.state = 571 _la = self._input.LA(1) if not(_la==17 or _la==94 or _la==169): self._errHandler.recoverInline(self) else: self._errHandler.reportMatch(self) self.consume() - self.state = 580 + self.state = 572 self.match(HogQLParser.STRING_LITERAL) - self.state = 581 + self.state = 573 self.match(HogQLParser.FROM) - self.state = 582 + self.state = 574 self.columnExpr(0) - self.state = 583 + self.state = 575 self.match(HogQLParser.RPAREN) pass @@ -5107,28 +5049,28 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprWinFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 585 + self.state = 577 self.identifier() - self.state = 586 + self.state = 578 self.match(HogQLParser.LPAREN) - self.state = 588 + self.state = 580 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 587 + self.state = 579 self.columnExprList() - self.state = 590 + self.state = 582 self.match(HogQLParser.RPAREN) - self.state = 592 + self.state = 584 self.match(HogQLParser.OVER) - self.state = 593 + self.state = 585 self.match(HogQLParser.LPAREN) - self.state = 594 + self.state = 586 self.windowExpr() - self.state = 595 + self.state = 587 self.match(HogQLParser.RPAREN) pass @@ -5136,24 +5078,24 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprWinFunctionTargetContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 597 + self.state = 589 self.identifier() - self.state = 598 + self.state = 590 self.match(HogQLParser.LPAREN) - self.state = 600 + self.state = 592 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 599 + self.state = 591 self.columnExprList() - self.state = 602 + self.state = 594 self.match(HogQLParser.RPAREN) - self.state = 604 + self.state = 596 self.match(HogQLParser.OVER) - self.state = 605 + self.state = 597 self.identifier() pass @@ -5161,45 +5103,45 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 607 + self.state = 599 self.identifier() - self.state = 613 + self.state = 605 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,75,self._ctx) + la_ = self._interp.adaptivePredict(self._input,74,self._ctx) if la_ == 1: - self.state = 608 + self.state = 600 self.match(HogQLParser.LPAREN) - self.state = 610 + self.state = 602 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 609 + self.state = 601 self.columnExprList() - self.state = 612 + self.state = 604 self.match(HogQLParser.RPAREN) - self.state = 615 + self.state = 607 self.match(HogQLParser.LPAREN) - self.state = 617 + self.state = 609 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,76,self._ctx) + la_ = self._interp.adaptivePredict(self._input,75,self._ctx) if la_ == 1: - self.state = 616 + self.state = 608 self.match(HogQLParser.DISTINCT) - self.state = 620 + self.state = 612 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 619 + self.state = 611 self.columnArgList() - self.state = 622 + self.state = 614 self.match(HogQLParser.RPAREN) pass @@ -5207,7 +5149,7 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprLiteralContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 624 + self.state = 616 self.literal() pass @@ -5215,9 +5157,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNegateContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 625 + self.state = 617 self.match(HogQLParser.DASH) - self.state = 626 + self.state = 618 self.columnExpr(17) pass @@ -5225,9 +5167,9 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprNotContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 627 + self.state = 619 self.match(HogQLParser.NOT) - self.state = 628 + self.state = 620 self.columnExpr(12) pass @@ -5235,17 +5177,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprAsteriskContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 632 + self.state = 624 self._errHandler.sync(self) _la = self._input.LA(1) if (((_la - 2)) & ~0x3f) == 0 and ((1 << (_la - 2)) & -1) != 0 or (((_la - 66)) & ~0x3f) == 0 and ((1 << (_la - 66)) & -633318697631745) != 0 or (((_la - 131)) & ~0x3f) == 0 and ((1 << (_la - 131)) & 6917529027641081855) != 0: - self.state = 629 + self.state = 621 self.tableIdentifier() - self.state = 630 + self.state = 622 self.match(HogQLParser.DOT) - self.state = 634 + self.state = 626 self.match(HogQLParser.ASTERISK) pass @@ -5253,11 +5195,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 635 + self.state = 627 self.match(HogQLParser.LPAREN) - self.state = 636 + self.state = 628 self.selectUnionStmt() - self.state = 637 + self.state = 629 self.match(HogQLParser.RPAREN) pass @@ -5265,11 +5207,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprParensContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 639 + self.state = 631 self.match(HogQLParser.LPAREN) - self.state = 640 + self.state = 632 self.columnExpr(0) - self.state = 641 + self.state = 633 self.match(HogQLParser.RPAREN) pass @@ -5277,11 +5219,11 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprTupleContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 643 + self.state = 635 self.match(HogQLParser.LPAREN) - self.state = 644 + self.state = 636 self.columnExprList() - self.state = 645 + self.state = 637 self.match(HogQLParser.RPAREN) pass @@ -5289,17 +5231,17 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprArrayContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 647 + self.state = 639 self.match(HogQLParser.LBRACKET) - self.state = 649 + self.state = 641 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 578896255) != 0: - self.state = 648 + self.state = 640 self.columnExprList() - self.state = 651 + self.state = 643 self.match(HogQLParser.RBRACKET) pass @@ -5307,50 +5249,50 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 652 + self.state = 644 self.columnIdentifier() pass self._ctx.stop = self._input.LT(-1) - self.state = 736 + self.state = 728 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,91,self._ctx) + _alt = self._interp.adaptivePredict(self._input,90,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: self.triggerExitRuleEvent() _prevctx = localctx - self.state = 734 + self.state = 726 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,90,self._ctx) + la_ = self._interp.adaptivePredict(self._input,89,self._ctx) if la_ == 1: localctx = HogQLParser.ColumnExprPrecedence1Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 655 + self.state = 647 if not self.precpred(self._ctx, 16): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 16)") - self.state = 659 + self.state = 651 self._errHandler.sync(self) token = self._input.LA(1) if token in [201]: - self.state = 656 + self.state = 648 localctx.operator = self.match(HogQLParser.ASTERISK) pass elif token in [230]: - self.state = 657 + self.state = 649 localctx.operator = self.match(HogQLParser.SLASH) pass elif token in [221]: - self.state = 658 + self.state = 650 localctx.operator = self.match(HogQLParser.PERCENT) pass else: raise NoViableAltException(self) - self.state = 661 + self.state = 653 localctx.right = self.columnExpr(17) pass @@ -5358,29 +5300,29 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence2Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 662 + self.state = 654 if not self.precpred(self._ctx, 15): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 15)") - self.state = 666 + self.state = 658 self._errHandler.sync(self) token = self._input.LA(1) if token in [222]: - self.state = 663 + self.state = 655 localctx.operator = self.match(HogQLParser.PLUS) pass elif token in [207]: - self.state = 664 + self.state = 656 localctx.operator = self.match(HogQLParser.DASH) pass elif token in [206]: - self.state = 665 + self.state = 657 localctx.operator = self.match(HogQLParser.CONCAT) pass else: raise NoViableAltException(self) - self.state = 668 + self.state = 660 localctx.right = self.columnExpr(16) pass @@ -5388,79 +5330,79 @@ def columnExpr(self, _p:int=0): localctx = HogQLParser.ColumnExprPrecedence3Context(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) localctx.left = _prevctx self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 669 + self.state = 661 if not self.precpred(self._ctx, 14): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 14)") - self.state = 688 + self.state = 680 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,86,self._ctx) + la_ = self._interp.adaptivePredict(self._input,85,self._ctx) if la_ == 1: - self.state = 670 + self.state = 662 localctx.operator = self.match(HogQLParser.EQ_DOUBLE) pass elif la_ == 2: - self.state = 671 + self.state = 663 localctx.operator = self.match(HogQLParser.EQ_SINGLE) pass elif la_ == 3: - self.state = 672 + self.state = 664 localctx.operator = self.match(HogQLParser.NOT_EQ) pass elif la_ == 4: - self.state = 673 + self.state = 665 localctx.operator = self.match(HogQLParser.LE) pass elif la_ == 5: - self.state = 674 + self.state = 666 localctx.operator = self.match(HogQLParser.GE) pass elif la_ == 6: - self.state = 675 + self.state = 667 localctx.operator = self.match(HogQLParser.LT) pass elif la_ == 7: - self.state = 676 + self.state = 668 localctx.operator = self.match(HogQLParser.GT) pass elif la_ == 8: - self.state = 678 + self.state = 670 self._errHandler.sync(self) _la = self._input.LA(1) if _la==70: - self.state = 677 + self.state = 669 localctx.operator = self.match(HogQLParser.GLOBAL) - self.state = 681 + self.state = 673 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114: - self.state = 680 + self.state = 672 self.match(HogQLParser.NOT) - self.state = 683 + self.state = 675 self.match(HogQLParser.IN) pass elif la_ == 9: - self.state = 685 + self.state = 677 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114: - self.state = 684 + self.state = 676 localctx.operator = self.match(HogQLParser.NOT) - self.state = 687 + self.state = 679 _la = self._input.LA(1) if not(_la==78 or _la==97): self._errHandler.recoverInline(self) @@ -5470,153 +5412,153 @@ def columnExpr(self, _p:int=0): pass - self.state = 690 + self.state = 682 localctx.right = self.columnExpr(15) pass elif la_ == 4: localctx = HogQLParser.ColumnExprAndContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 691 + self.state = 683 if not self.precpred(self._ctx, 11): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 11)") - self.state = 692 + self.state = 684 self.match(HogQLParser.AND) - self.state = 693 + self.state = 685 self.columnExpr(12) pass elif la_ == 5: localctx = HogQLParser.ColumnExprOrContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 694 + self.state = 686 if not self.precpred(self._ctx, 10): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 10)") - self.state = 695 + self.state = 687 self.match(HogQLParser.OR) - self.state = 696 + self.state = 688 self.columnExpr(11) pass elif la_ == 6: localctx = HogQLParser.ColumnExprBetweenContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 697 + self.state = 689 if not self.precpred(self._ctx, 9): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 9)") - self.state = 699 + self.state = 691 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114: - self.state = 698 + self.state = 690 self.match(HogQLParser.NOT) - self.state = 701 + self.state = 693 self.match(HogQLParser.BETWEEN) - self.state = 702 + self.state = 694 self.columnExpr(0) - self.state = 703 + self.state = 695 self.match(HogQLParser.AND) - self.state = 704 + self.state = 696 self.columnExpr(10) pass elif la_ == 7: localctx = HogQLParser.ColumnExprTernaryOpContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 706 + self.state = 698 if not self.precpred(self._ctx, 8): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 8)") - self.state = 707 + self.state = 699 self.match(HogQLParser.QUERY) - self.state = 708 + self.state = 700 self.columnExpr(0) - self.state = 709 + self.state = 701 self.match(HogQLParser.COLON) - self.state = 710 + self.state = 702 self.columnExpr(8) pass elif la_ == 8: localctx = HogQLParser.ColumnExprArrayAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 712 + self.state = 704 if not self.precpred(self._ctx, 19): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 19)") - self.state = 713 + self.state = 705 self.match(HogQLParser.LBRACKET) - self.state = 714 + self.state = 706 self.columnExpr(0) - self.state = 715 + self.state = 707 self.match(HogQLParser.RBRACKET) pass elif la_ == 9: localctx = HogQLParser.ColumnExprTupleAccessContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 717 + self.state = 709 if not self.precpred(self._ctx, 18): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 18)") - self.state = 718 + self.state = 710 self.match(HogQLParser.DOT) - self.state = 719 + self.state = 711 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 10: localctx = HogQLParser.ColumnExprIsNullContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 720 + self.state = 712 if not self.precpred(self._ctx, 13): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 13)") - self.state = 721 + self.state = 713 self.match(HogQLParser.IS) - self.state = 723 + self.state = 715 self._errHandler.sync(self) _la = self._input.LA(1) if _la==114: - self.state = 722 + self.state = 714 self.match(HogQLParser.NOT) - self.state = 725 + self.state = 717 self.match(HogQLParser.NULL_SQL) pass elif la_ == 11: localctx = HogQLParser.ColumnExprAliasContext(self, HogQLParser.ColumnExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_columnExpr) - self.state = 726 + self.state = 718 if not self.precpred(self._ctx, 7): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 7)") - self.state = 732 + self.state = 724 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,89,self._ctx) + la_ = self._interp.adaptivePredict(self._input,88,self._ctx) if la_ == 1: - self.state = 727 + self.state = 719 self.alias() pass elif la_ == 2: - self.state = 728 + self.state = 720 self.match(HogQLParser.AS) - self.state = 729 + self.state = 721 self.identifier() pass elif la_ == 3: - self.state = 730 + self.state = 722 self.match(HogQLParser.AS) - self.state = 731 + self.state = 723 self.match(HogQLParser.STRING_LITERAL) pass @@ -5624,9 +5566,9 @@ def columnExpr(self, _p:int=0): pass - self.state = 738 + self.state = 730 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,91,self._ctx) + _alt = self._interp.adaptivePredict(self._input,90,self._ctx) except RecognitionException as re: localctx.exception = re @@ -5672,21 +5614,21 @@ def accept(self, visitor:ParseTreeVisitor): def columnArgList(self): localctx = HogQLParser.ColumnArgListContext(self, self._ctx, self.state) - self.enterRule(localctx, 80, self.RULE_columnArgList) + self.enterRule(localctx, 78, self.RULE_columnArgList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 739 + self.state = 731 self.columnArgExpr() - self.state = 744 + self.state = 736 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 740 + self.state = 732 self.match(HogQLParser.COMMA) - self.state = 741 + self.state = 733 self.columnArgExpr() - self.state = 746 + self.state = 738 self._errHandler.sync(self) _la = self._input.LA(1) @@ -5729,20 +5671,20 @@ def accept(self, visitor:ParseTreeVisitor): def columnArgExpr(self): localctx = HogQLParser.ColumnArgExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 82, self.RULE_columnArgExpr) + self.enterRule(localctx, 80, self.RULE_columnArgExpr) try: - self.state = 749 + self.state = 741 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,93,self._ctx) + la_ = self._interp.adaptivePredict(self._input,92,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 747 + self.state = 739 self.columnLambdaExpr() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 748 + self.state = 740 self.columnExpr(0) pass @@ -5804,45 +5746,45 @@ def accept(self, visitor:ParseTreeVisitor): def columnLambdaExpr(self): localctx = HogQLParser.ColumnLambdaExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 84, self.RULE_columnLambdaExpr) + self.enterRule(localctx, 82, self.RULE_columnLambdaExpr) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 770 + self.state = 762 self._errHandler.sync(self) token = self._input.LA(1) if token in [218]: - self.state = 751 + self.state = 743 self.match(HogQLParser.LPAREN) - self.state = 752 + self.state = 744 self.identifier() - self.state = 757 + self.state = 749 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 753 + self.state = 745 self.match(HogQLParser.COMMA) - self.state = 754 + self.state = 746 self.identifier() - self.state = 759 + self.state = 751 self._errHandler.sync(self) _la = self._input.LA(1) - self.state = 760 + self.state = 752 self.match(HogQLParser.RPAREN) pass elif token in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 113, 114, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 193]: - self.state = 762 + self.state = 754 self.identifier() - self.state = 767 + self.state = 759 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 763 + self.state = 755 self.match(HogQLParser.COMMA) - self.state = 764 + self.state = 756 self.identifier() - self.state = 769 + self.state = 761 self._errHandler.sync(self) _la = self._input.LA(1) @@ -5850,9 +5792,9 @@ def columnLambdaExpr(self): else: raise NoViableAltException(self) - self.state = 772 + self.state = 764 self.match(HogQLParser.ARROW) - self.state = 773 + self.state = 765 self.columnExpr(0) except RecognitionException as re: localctx.exception = re @@ -5899,29 +5841,29 @@ def accept(self, visitor:ParseTreeVisitor): def columnIdentifier(self): localctx = HogQLParser.ColumnIdentifierContext(self, self._ctx, self.state) - self.enterRule(localctx, 86, self.RULE_columnIdentifier) + self.enterRule(localctx, 84, self.RULE_columnIdentifier) try: - self.state = 782 + self.state = 774 self._errHandler.sync(self) token = self._input.LA(1) if token in [199]: self.enterOuterAlt(localctx, 1) - self.state = 775 + self.state = 767 self.match(HogQLParser.PLACEHOLDER) pass elif token in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 113, 114, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 193]: self.enterOuterAlt(localctx, 2) - self.state = 779 + self.state = 771 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,97,self._ctx) + la_ = self._interp.adaptivePredict(self._input,96,self._ctx) if la_ == 1: - self.state = 776 + self.state = 768 self.tableIdentifier() - self.state = 777 + self.state = 769 self.match(HogQLParser.DOT) - self.state = 781 + self.state = 773 self.nestedIdentifier() pass else: @@ -5971,23 +5913,23 @@ def accept(self, visitor:ParseTreeVisitor): def nestedIdentifier(self): localctx = HogQLParser.NestedIdentifierContext(self, self._ctx, self.state) - self.enterRule(localctx, 88, self.RULE_nestedIdentifier) + self.enterRule(localctx, 86, self.RULE_nestedIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 784 + self.state = 776 self.identifier() - self.state = 789 + self.state = 781 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,99,self._ctx) + _alt = self._interp.adaptivePredict(self._input,98,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: - self.state = 785 + self.state = 777 self.match(HogQLParser.DOT) - self.state = 786 + self.state = 778 self.identifier() - self.state = 791 + self.state = 783 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,99,self._ctx) + _alt = self._interp.adaptivePredict(self._input,98,self._ctx) except RecognitionException as re: localctx.exception = re @@ -6100,19 +6042,19 @@ def tableExpr(self, _p:int=0): _parentState = self.state localctx = HogQLParser.TableExprContext(self, self._ctx, _parentState) _prevctx = localctx - _startState = 90 - self.enterRecursionRule(localctx, 90, self.RULE_tableExpr, _p) + _startState = 88 + self.enterRecursionRule(localctx, 88, self.RULE_tableExpr, _p) try: self.enterOuterAlt(localctx, 1) - self.state = 799 + self.state = 791 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,100,self._ctx) + la_ = self._interp.adaptivePredict(self._input,99,self._ctx) if la_ == 1: localctx = HogQLParser.TableExprIdentifierContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 793 + self.state = 785 self.tableIdentifier() pass @@ -6120,7 +6062,7 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprFunctionContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 794 + self.state = 786 self.tableFunctionExpr() pass @@ -6128,19 +6070,19 @@ def tableExpr(self, _p:int=0): localctx = HogQLParser.TableExprSubqueryContext(self, localctx) self._ctx = localctx _prevctx = localctx - self.state = 795 + self.state = 787 self.match(HogQLParser.LPAREN) - self.state = 796 + self.state = 788 self.selectUnionStmt() - self.state = 797 + self.state = 789 self.match(HogQLParser.RPAREN) pass self._ctx.stop = self._input.LT(-1) - self.state = 809 + self.state = 801 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,102,self._ctx) + _alt = self._interp.adaptivePredict(self._input,101,self._ctx) while _alt!=2 and _alt!=ATN.INVALID_ALT_NUMBER: if _alt==1: if self._parseListeners is not None: @@ -6148,29 +6090,29 @@ def tableExpr(self, _p:int=0): _prevctx = localctx localctx = HogQLParser.TableExprAliasContext(self, HogQLParser.TableExprContext(self, _parentctx, _parentState)) self.pushNewRecursionContext(localctx, _startState, self.RULE_tableExpr) - self.state = 801 + self.state = 793 if not self.precpred(self._ctx, 1): from antlr4.error.Errors import FailedPredicateException raise FailedPredicateException(self, "self.precpred(self._ctx, 1)") - self.state = 805 + self.state = 797 self._errHandler.sync(self) token = self._input.LA(1) if token in [35, 61, 76, 90, 193]: - self.state = 802 + self.state = 794 self.alias() pass elif token in [10]: - self.state = 803 + self.state = 795 self.match(HogQLParser.AS) - self.state = 804 + self.state = 796 self.identifier() pass else: raise NoViableAltException(self) - self.state = 811 + self.state = 803 self._errHandler.sync(self) - _alt = self._interp.adaptivePredict(self._input,102,self._ctx) + _alt = self._interp.adaptivePredict(self._input,101,self._ctx) except RecognitionException as re: localctx.exception = re @@ -6217,23 +6159,23 @@ def accept(self, visitor:ParseTreeVisitor): def tableFunctionExpr(self): localctx = HogQLParser.TableFunctionExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 92, self.RULE_tableFunctionExpr) + self.enterRule(localctx, 90, self.RULE_tableFunctionExpr) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 812 + self.state = 804 self.identifier() - self.state = 813 + self.state = 805 self.match(HogQLParser.LPAREN) - self.state = 815 + self.state = 807 self._errHandler.sync(self) _la = self._input.LA(1) if ((_la) & ~0x3f) == 0 and ((1 << _la) & -4) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -1) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -5) != 0 or (((_la - 193)) & ~0x3f) == 0 and ((1 << (_la - 193)) & 536952895) != 0: - self.state = 814 + self.state = 806 self.tableArgList() - self.state = 817 + self.state = 809 self.match(HogQLParser.RPAREN) except RecognitionException as re: localctx.exception = re @@ -6277,20 +6219,20 @@ def accept(self, visitor:ParseTreeVisitor): def tableIdentifier(self): localctx = HogQLParser.TableIdentifierContext(self, self._ctx, self.state) - self.enterRule(localctx, 94, self.RULE_tableIdentifier) + self.enterRule(localctx, 92, self.RULE_tableIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 822 + self.state = 814 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,104,self._ctx) + la_ = self._interp.adaptivePredict(self._input,103,self._ctx) if la_ == 1: - self.state = 819 + self.state = 811 self.databaseIdentifier() - self.state = 820 + self.state = 812 self.match(HogQLParser.DOT) - self.state = 824 + self.state = 816 self.identifier() except RecognitionException as re: localctx.exception = re @@ -6336,21 +6278,21 @@ def accept(self, visitor:ParseTreeVisitor): def tableArgList(self): localctx = HogQLParser.TableArgListContext(self, self._ctx, self.state) - self.enterRule(localctx, 96, self.RULE_tableArgList) + self.enterRule(localctx, 94, self.RULE_tableArgList) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 826 + self.state = 818 self.tableArgExpr() - self.state = 831 + self.state = 823 self._errHandler.sync(self) _la = self._input.LA(1) while _la==205: - self.state = 827 + self.state = 819 self.match(HogQLParser.COMMA) - self.state = 828 + self.state = 820 self.tableArgExpr() - self.state = 833 + self.state = 825 self._errHandler.sync(self) _la = self._input.LA(1) @@ -6397,26 +6339,26 @@ def accept(self, visitor:ParseTreeVisitor): def tableArgExpr(self): localctx = HogQLParser.TableArgExprContext(self, self._ctx, self.state) - self.enterRule(localctx, 98, self.RULE_tableArgExpr) + self.enterRule(localctx, 96, self.RULE_tableArgExpr) try: - self.state = 837 + self.state = 829 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,106,self._ctx) + la_ = self._interp.adaptivePredict(self._input,105,self._ctx) if la_ == 1: self.enterOuterAlt(localctx, 1) - self.state = 834 + self.state = 826 self.nestedIdentifier() pass elif la_ == 2: self.enterOuterAlt(localctx, 2) - self.state = 835 + self.state = 827 self.tableFunctionExpr() pass elif la_ == 3: self.enterOuterAlt(localctx, 3) - self.state = 836 + self.state = 828 self.literal() pass @@ -6456,10 +6398,10 @@ def accept(self, visitor:ParseTreeVisitor): def databaseIdentifier(self): localctx = HogQLParser.DatabaseIdentifierContext(self, self._ctx, self.state) - self.enterRule(localctx, 100, self.RULE_databaseIdentifier) + self.enterRule(localctx, 98, self.RULE_databaseIdentifier) try: self.enterOuterAlt(localctx, 1) - self.state = 839 + self.state = 831 self.identifier() except RecognitionException as re: localctx.exception = re @@ -6507,22 +6449,22 @@ def accept(self, visitor:ParseTreeVisitor): def floatingLiteral(self): localctx = HogQLParser.FloatingLiteralContext(self, self._ctx, self.state) - self.enterRule(localctx, 102, self.RULE_floatingLiteral) + self.enterRule(localctx, 100, self.RULE_floatingLiteral) self._la = 0 # Token type try: - self.state = 849 + self.state = 841 self._errHandler.sync(self) token = self._input.LA(1) if token in [194]: self.enterOuterAlt(localctx, 1) - self.state = 841 + self.state = 833 self.match(HogQLParser.FLOATING_LITERAL) pass elif token in [209]: self.enterOuterAlt(localctx, 2) - self.state = 842 + self.state = 834 self.match(HogQLParser.DOT) - self.state = 843 + self.state = 835 _la = self._input.LA(1) if not(_la==195 or _la==196): self._errHandler.recoverInline(self) @@ -6532,15 +6474,15 @@ def floatingLiteral(self): pass elif token in [196]: self.enterOuterAlt(localctx, 3) - self.state = 844 + self.state = 836 self.match(HogQLParser.DECIMAL_LITERAL) - self.state = 845 + self.state = 837 self.match(HogQLParser.DOT) - self.state = 847 + self.state = 839 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,107,self._ctx) + la_ = self._interp.adaptivePredict(self._input,106,self._ctx) if la_ == 1: - self.state = 846 + self.state = 838 _la = self._input.LA(1) if not(_la==195 or _la==196): self._errHandler.recoverInline(self) @@ -6609,15 +6551,15 @@ def accept(self, visitor:ParseTreeVisitor): def numberLiteral(self): localctx = HogQLParser.NumberLiteralContext(self, self._ctx, self.state) - self.enterRule(localctx, 104, self.RULE_numberLiteral) + self.enterRule(localctx, 102, self.RULE_numberLiteral) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 852 + self.state = 844 self._errHandler.sync(self) _la = self._input.LA(1) if _la==207 or _la==222: - self.state = 851 + self.state = 843 _la = self._input.LA(1) if not(_la==207 or _la==222): self._errHandler.recoverInline(self) @@ -6626,36 +6568,36 @@ def numberLiteral(self): self.consume() - self.state = 860 + self.state = 852 self._errHandler.sync(self) - la_ = self._interp.adaptivePredict(self._input,110,self._ctx) + la_ = self._interp.adaptivePredict(self._input,109,self._ctx) if la_ == 1: - self.state = 854 + self.state = 846 self.floatingLiteral() pass elif la_ == 2: - self.state = 855 + self.state = 847 self.match(HogQLParser.OCTAL_LITERAL) pass elif la_ == 3: - self.state = 856 + self.state = 848 self.match(HogQLParser.DECIMAL_LITERAL) pass elif la_ == 4: - self.state = 857 + self.state = 849 self.match(HogQLParser.HEXADECIMAL_LITERAL) pass elif la_ == 5: - self.state = 858 + self.state = 850 self.match(HogQLParser.INF) pass elif la_ == 6: - self.state = 859 + self.state = 851 self.match(HogQLParser.NAN_SQL) pass @@ -6701,24 +6643,24 @@ def accept(self, visitor:ParseTreeVisitor): def literal(self): localctx = HogQLParser.LiteralContext(self, self._ctx, self.state) - self.enterRule(localctx, 106, self.RULE_literal) + self.enterRule(localctx, 104, self.RULE_literal) try: - self.state = 865 + self.state = 857 self._errHandler.sync(self) token = self._input.LA(1) if token in [81, 112, 194, 195, 196, 197, 207, 209, 222]: self.enterOuterAlt(localctx, 1) - self.state = 862 + self.state = 854 self.numberLiteral() pass elif token in [198]: self.enterOuterAlt(localctx, 2) - self.state = 863 + self.state = 855 self.match(HogQLParser.STRING_LITERAL) pass elif token in [115]: self.enterOuterAlt(localctx, 3) - self.state = 864 + self.state = 856 self.match(HogQLParser.NULL_SQL) pass else: @@ -6779,11 +6721,11 @@ def accept(self, visitor:ParseTreeVisitor): def interval(self): localctx = HogQLParser.IntervalContext(self, self._ctx, self.state) - self.enterRule(localctx, 108, self.RULE_interval) + self.enterRule(localctx, 106, self.RULE_interval) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 867 + self.state = 859 _la = self._input.LA(1) if not(_la==36 or (((_la - 75)) & ~0x3f) == 0 and ((1 << (_la - 75)) & 72057615512764417) != 0 or (((_la - 144)) & ~0x3f) == 0 and ((1 << (_la - 144)) & 36283883716609) != 0): self._errHandler.recoverInline(self) @@ -7355,11 +7297,11 @@ def accept(self, visitor:ParseTreeVisitor): def keyword(self): localctx = HogQLParser.KeywordContext(self, self._ctx, self.state) - self.enterRule(localctx, 110, self.RULE_keyword) + self.enterRule(localctx, 108, self.RULE_keyword) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 869 + self.state = 861 _la = self._input.LA(1) if not(((_la) & ~0x3f) == 0 and ((1 << _la) & -68719476740) != 0 or (((_la - 64)) & ~0x3f) == 0 and ((1 << (_la - 64)) & -2577255255640065) != 0 or (((_la - 128)) & ~0x3f) == 0 and ((1 << (_la - 128)) & -2377900603251687437) != 0): self._errHandler.recoverInline(self) @@ -7409,11 +7351,11 @@ def accept(self, visitor:ParseTreeVisitor): def keywordForAlias(self): localctx = HogQLParser.KeywordForAliasContext(self, self._ctx, self.state) - self.enterRule(localctx, 112, self.RULE_keywordForAlias) + self.enterRule(localctx, 110, self.RULE_keywordForAlias) self._la = 0 # Token type try: self.enterOuterAlt(localctx, 1) - self.state = 871 + self.state = 863 _la = self._input.LA(1) if not((((_la - 35)) & ~0x3f) == 0 and ((1 << (_la - 35)) & 36030996109328385) != 0): self._errHandler.recoverInline(self) @@ -7458,19 +7400,19 @@ def accept(self, visitor:ParseTreeVisitor): def alias(self): localctx = HogQLParser.AliasContext(self, self._ctx, self.state) - self.enterRule(localctx, 114, self.RULE_alias) + self.enterRule(localctx, 112, self.RULE_alias) try: - self.state = 875 + self.state = 867 self._errHandler.sync(self) token = self._input.LA(1) if token in [193]: self.enterOuterAlt(localctx, 1) - self.state = 873 + self.state = 865 self.match(HogQLParser.IDENTIFIER) pass elif token in [35, 61, 76, 90]: self.enterOuterAlt(localctx, 2) - self.state = 874 + self.state = 866 self.keywordForAlias() pass else: @@ -7518,24 +7460,24 @@ def accept(self, visitor:ParseTreeVisitor): def identifier(self): localctx = HogQLParser.IdentifierContext(self, self._ctx, self.state) - self.enterRule(localctx, 116, self.RULE_identifier) + self.enterRule(localctx, 114, self.RULE_identifier) try: - self.state = 880 + self.state = 872 self._errHandler.sync(self) token = self._input.LA(1) if token in [193]: self.enterOuterAlt(localctx, 1) - self.state = 877 + self.state = 869 self.match(HogQLParser.IDENTIFIER) pass elif token in [36, 75, 107, 109, 131, 144, 184, 189]: self.enterOuterAlt(localctx, 2) - self.state = 878 + self.state = 870 self.interval() pass elif token in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 76, 77, 78, 79, 80, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 108, 110, 111, 113, 114, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 185, 186, 187, 188, 190, 191]: self.enterOuterAlt(localctx, 3) - self.state = 879 + self.state = 871 self.keyword() pass else: @@ -7579,19 +7521,19 @@ def accept(self, visitor:ParseTreeVisitor): def identifierOrNull(self): localctx = HogQLParser.IdentifierOrNullContext(self, self._ctx, self.state) - self.enterRule(localctx, 118, self.RULE_identifierOrNull) + self.enterRule(localctx, 116, self.RULE_identifierOrNull) try: - self.state = 884 + self.state = 876 self._errHandler.sync(self) token = self._input.LA(1) if token in [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 113, 114, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 193]: self.enterOuterAlt(localctx, 1) - self.state = 882 + self.state = 874 self.identifier() pass elif token in [115]: self.enterOuterAlt(localctx, 2) - self.state = 883 + self.state = 875 self.match(HogQLParser.NULL_SQL) pass else: @@ -7638,14 +7580,14 @@ def accept(self, visitor:ParseTreeVisitor): def enumValue(self): localctx = HogQLParser.EnumValueContext(self, self._ctx, self.state) - self.enterRule(localctx, 120, self.RULE_enumValue) + self.enterRule(localctx, 118, self.RULE_enumValue) try: self.enterOuterAlt(localctx, 1) - self.state = 886 + self.state = 878 self.match(HogQLParser.STRING_LITERAL) - self.state = 887 + self.state = 879 self.match(HogQLParser.EQ_SINGLE) - self.state = 888 + self.state = 880 self.numberLiteral() except RecognitionException as re: localctx.exception = re @@ -7660,9 +7602,9 @@ def enumValue(self): def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): if self._predicates == None: self._predicates = dict() - self._predicates[18] = self.joinExpr_sempred - self._predicates[39] = self.columnExpr_sempred - self._predicates[45] = self.tableExpr_sempred + self._predicates[17] = self.joinExpr_sempred + self._predicates[38] = self.columnExpr_sempred + self._predicates[44] = self.tableExpr_sempred pred = self._predicates.get(ruleIndex, None) if pred is None: raise Exception("No predicate with index:" + str(ruleIndex)) diff --git a/posthog/hogql/grammar/HogQLParserVisitor.py b/posthog/hogql/grammar/HogQLParserVisitor.py index dbaf69ef6fda9..b5bc53c977fc1 100644 --- a/posthog/hogql/grammar/HogQLParserVisitor.py +++ b/posthog/hogql/grammar/HogQLParserVisitor.py @@ -84,11 +84,6 @@ def visitProjectionOrderByClause(self, ctx:HogQLParser.ProjectionOrderByClauseCo return self.visitChildren(ctx) - # Visit a parse tree produced by HogQLParser#limitByClause. - def visitLimitByClause(self, ctx:HogQLParser.LimitByClauseContext): - return self.visitChildren(ctx) - - # Visit a parse tree produced by HogQLParser#limitClause. def visitLimitClause(self, ctx:HogQLParser.LimitClauseContext): return self.visitChildren(ctx) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index f393769960196..a8347a36a0035 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -66,17 +66,17 @@ def visitSelectStmt(self, ctx: HogQLParser.SelectStmtContext): order_by=self.visit(ctx.orderByClause()) if ctx.orderByClause() else None, ) - any_limit_clause = ctx.limitClause() or ctx.limitByClause() - if any_limit_clause and any_limit_clause.limitExpr(): - limit_expr = any_limit_clause.limitExpr() + if ctx.limitClause(): + limit_clause = ctx.limitClause() + limit_expr = limit_clause.limitExpr() if limit_expr.columnExpr(0): select_query.limit = self.visit(limit_expr.columnExpr(0)) if limit_expr.columnExpr(1): select_query.offset = self.visit(limit_expr.columnExpr(1)) - if ctx.limitClause() and ctx.limitClause().WITH() and ctx.limitClause().TIES(): - select_query.limit_with_ties = True - if ctx.limitByClause() and ctx.limitByClause().columnExprList(): - select_query.limit_by = self.visit(ctx.limitByClause().columnExprList()) + if limit_clause.columnExprList(): + select_query.limit_by = self.visit(limit_clause.columnExprList()) + if limit_clause.WITH() and limit_clause.TIES(): + select_query.limit_with_ties = True if ctx.withClause(): raise NotImplementedError(f"Unsupported: SelectStmt.withClause()") @@ -124,9 +124,6 @@ def visitOrderByClause(self, ctx: HogQLParser.OrderByClauseContext): def visitProjectionOrderByClause(self, ctx: HogQLParser.ProjectionOrderByClauseContext): raise NotImplementedError(f"Unsupported node: ProjectionOrderByClause") - def visitLimitByClause(self, ctx: HogQLParser.LimitByClauseContext): - raise Exception(f"Parsed as part of SelectStmt, can't parse directly.") - def visitLimitClause(self, ctx: HogQLParser.LimitClauseContext): raise Exception(f"Parsed as part of SelectStmt, can't parse directly.") diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index e2ebe83dc4d23..65eb95b928eba 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -88,7 +88,7 @@ def print_ast( f"ORDER BY {', '.join(order_by)}" if order_by and len(order_by) > 0 else None, ] if limit is not None: - clauses.append(f"LIMIT {print_ast(limit, stack, context, dialect)}"), + clauses.append(f"LIMIT {print_ast(limit, stack, context, dialect)}") if node.offset is not None: clauses.append(f"OFFSET {print_ast(node.offset, stack, context, dialect)}") if node.limit_by is not None: From 9cd96bccedc8ae501140385b8233ea5fabd6ac51 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Feb 2023 15:27:10 +0000 Subject: [PATCH 007/142] Update snapshots --- .../queries/trends/test/__snapshots__/test_formula.ambr | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/posthog/queries/trends/test/__snapshots__/test_formula.ambr b/posthog/queries/trends/test/__snapshots__/test_formula.ambr index 3ce2fa339fefa..6828f1b8f59cd 100644 --- a/posthog/queries/trends/test/__snapshots__/test_formula.ambr +++ b/posthog/queries/trends/test/__snapshots__/test_formula.ambr @@ -153,7 +153,7 @@ CROSS JOIN (SELECT breakdown_value FROM - (SELECT [36, 0] as breakdown_value) ARRAY + (SELECT [37, 0] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start @@ -163,7 +163,7 @@ FROM events e INNER JOIN (SELECT distinct_id, - 36 as value + 37 as value FROM (SELECT distinct_id, argMax(person_id, version) as person_id @@ -222,7 +222,7 @@ CROSS JOIN (SELECT breakdown_value FROM - (SELECT [36, 0] as breakdown_value) ARRAY + (SELECT [37, 0] as breakdown_value) ARRAY JOIN breakdown_value) as sec ORDER BY breakdown_value, day_start @@ -232,7 +232,7 @@ FROM events e INNER JOIN (SELECT distinct_id, - 36 as value + 37 as value FROM (SELECT distinct_id, argMax(person_id, version) as person_id From 2d0d1e84605e5578bb665b86380d7b3bde31a331 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 8 Feb 2023 21:45:12 +0100 Subject: [PATCH 008/142] fix placeholders --- posthog/hogql/hogql.py | 6 +++--- posthog/hogql/parser.py | 11 ++++++++--- posthog/hogql/placeholders.py | 9 +++++++++ posthog/hogql/test/test_placeholders.py | 17 ++++++++++++++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index dc16b3cf8e9f6..8e216b1654b46 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -6,15 +6,15 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", "clickhouse"] = "clickhouse") -> str: - """Translate a HogQL expression into a Clickhouse expression.""" + """Translate a HogQL expression into a Clickhouse expression. Raises if any placeholders found.""" if query == "": raise ValueError("Empty query") try: if context.select_team_id: - node = parse_select(query) + node = parse_select(query, no_placeholders=True) else: - node = parse_expr(query) + node = parse_expr(query, no_placeholders=True) except SyntaxError as err: raise ValueError(f"SyntaxError: {err.msg}") except NotImplementedError as err: diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index a8347a36a0035..a2646a16d6973 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -7,22 +7,27 @@ from posthog.hogql.grammar.HogQLLexer import HogQLLexer from posthog.hogql.grammar.HogQLParser import HogQLParser from posthog.hogql.parse_string import parse_string, parse_string_literal -from posthog.hogql.placeholders import replace_placeholders +from posthog.hogql.placeholders import assert_no_placeholders, replace_placeholders -def parse_expr(expr: str, placeholders: Optional[Dict[str, ast.Expr]] = None) -> ast.Expr: +def parse_expr(expr: str, placeholders: Optional[Dict[str, ast.Expr]] = None, no_placeholders=False) -> ast.Expr: parse_tree = get_parser(expr).expr() node = HogQLParseTreeConverter().visit(parse_tree) if placeholders: return replace_placeholders(node, placeholders) + elif no_placeholders: + assert_no_placeholders(node) + return node -def parse_select(statement: str, placeholders: Optional[Dict[str, ast.Expr]] = None) -> ast.Expr: +def parse_select(statement: str, placeholders: Optional[Dict[str, ast.Expr]] = None, no_placeholders=False) -> ast.Expr: parse_tree = get_parser(statement).select() node = HogQLParseTreeConverter().visit(parse_tree) if placeholders: node = replace_placeholders(node, placeholders) + elif no_placeholders: + assert_no_placeholders(node) return node diff --git a/posthog/hogql/placeholders.py b/posthog/hogql/placeholders.py index a3b543e2a0804..0399f6dc2d498 100644 --- a/posthog/hogql/placeholders.py +++ b/posthog/hogql/placeholders.py @@ -16,3 +16,12 @@ def visit_placeholder(self, node): if node.field in self.placeholders: return self.placeholders[node.field] raise ValueError(f"Placeholder '{node.field}' not found in provided dict: {', '.join(list(self.placeholders))}") + + +def assert_no_placeholders(node: ast.Expr): + AssertNoPlaceholders().visit(node) + + +class AssertNoPlaceholders(EverythingVisitor): + def visit_placeholder(self, node): + raise ValueError(f"Placeholder '{node.field}' not allowed in this context") diff --git a/posthog/hogql/test/test_placeholders.py b/posthog/hogql/test/test_placeholders.py index 4211c00238dfc..b7a8211c2ada4 100644 --- a/posthog/hogql/test/test_placeholders.py +++ b/posthog/hogql/test/test_placeholders.py @@ -1,6 +1,6 @@ from posthog.hogql import ast from posthog.hogql.parser import parse_expr -from posthog.hogql.placeholders import replace_placeholders +from posthog.hogql.placeholders import assert_no_placeholders, replace_placeholders from posthog.test.base import BaseTest @@ -17,6 +17,15 @@ def test_replace_placeholders_simple(self): ast.Constant(value="bar"), ) + def test_replace_placeholders_error(self): + expr = ast.Placeholder(field="foo") + with self.assertRaises(ValueError) as context: + replace_placeholders(expr, {}) + self.assertTrue("Placeholder 'foo' not found in provided dict:" in str(context.exception)) + with self.assertRaises(ValueError) as context: + replace_placeholders(expr, {"bar": ast.Constant(value=123)}) + self.assertTrue("Placeholder 'foo' not found in provided dict: bar" in str(context.exception)) + def test_replace_placeholders_comparison(self): expr = parse_expr("timestamp < {timestamp}") self.assertEqual( @@ -36,3 +45,9 @@ def test_replace_placeholders_comparison(self): right=ast.Constant(value=123), ), ) + + def test_assert_no_placeholders(self): + expr = ast.Placeholder(field="foo") + with self.assertRaises(ValueError) as context: + assert_no_placeholders(expr) + self.assertTrue("Placeholder 'foo' not allowed in this context" in str(context.exception)) From 5064dad837469816390b2d18286aa57ecd7f63e9 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 11:04:13 +0100 Subject: [PATCH 009/142] resolve symbols for events table --- posthog/hogql/ast.py | 75 ++++++++++++++++- posthog/hogql/parser.py | 4 +- posthog/hogql/placeholders.py | 6 +- posthog/hogql/resolver.py | 122 ++++++++++++++++++++++++++++ posthog/hogql/test/test_parser.py | 14 ++++ posthog/hogql/test/test_resolver.py | 79 ++++++++++++++++++ posthog/hogql/test/test_visitor.py | 6 +- posthog/hogql/visitor.py | 76 ++++++++++++++++- 8 files changed, 368 insertions(+), 14 deletions(-) create mode 100644 posthog/hogql/resolver.py create mode 100644 posthog/hogql/test/test_resolver.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 012a9b9a8b57a..76cea55d685ad 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -1,10 +1,12 @@ import re from enum import Enum -from typing import Any, List, Literal, Optional, Union +from typing import Any, Dict, List, Literal, Optional, Union from pydantic import BaseModel, Extra -# NOTE: when you add new AST fields or nodes, add them to EverythingVisitor as well! +from posthog.hogql.constants import EVENT_FIELDS + +# NOTE: when you add new AST fields or nodes, add them to CloningVisitor as well! camel_case_pattern = re.compile(r"(? "Symbol": + raise NotImplementedError() + + def has_child(self, name: str) -> bool: + return self.get_child(name) is not None + + +class AliasSymbol(Symbol): + expr: "Expr" + + def get_child(self, name: str) -> "Symbol": + if isinstance(self.expr, SelectQuery): + return self.expr.symbol.get_child(name) + elif isinstance(self.expr, Field): + return self.expr.symbol.get_child(name) + + +class TableSymbol(Symbol): + table_name: Literal["events"] + + def has_child(self, name: str) -> bool: + if self.table_name == "events": + return name in EVENT_FIELDS + else: + raise NotImplementedError(f"Can not resolve table: {self.name}") + + def get_child(self, name: str) -> "Symbol": + if self.table_name == "events": + if name in EVENT_FIELDS: + return FieldSymbol(name=name, table=self) + else: + raise NotImplementedError(f"Can not resolve table: {self.name}") + + +class SelectQuerySymbol(Symbol): + # expr: "Expr" + + symbols: Dict[str, Symbol] + tables: Dict[str, Symbol] + + +class FieldSymbol(Symbol): + table: TableSymbol + + def get_child(self, name: str) -> "Symbol": + if self.table.table_name == "events": + if self.name == "properties": + raise NotImplementedError(f"Property symbol resolution not implemented yet") + else: + raise NotImplementedError(f"Can not resolve field {self.name} on table events") + else: + raise NotImplementedError(f"Can not resolve fields on table: {self.name}") + self.table.get_child(name) + + +class PropertySymbol(Symbol): + field: FieldSymbol + + class Expr(AST): - pass + symbol: Optional[Symbol] + + +Symbol.update_forward_refs(Expr=Expr) +AliasSymbol.update_forward_refs(Expr=Expr) +SelectQuerySymbol.update_forward_refs(Expr=Expr) class Alias(Expr): diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index a2646a16d6973..0f2424bb6d9cb 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -21,7 +21,9 @@ def parse_expr(expr: str, placeholders: Optional[Dict[str, ast.Expr]] = None, no return node -def parse_select(statement: str, placeholders: Optional[Dict[str, ast.Expr]] = None, no_placeholders=False) -> ast.Expr: +def parse_select( + statement: str, placeholders: Optional[Dict[str, ast.Expr]] = None, no_placeholders=False +) -> ast.SelectQuery: parse_tree = get_parser(statement).select() node = HogQLParseTreeConverter().visit(parse_tree) if placeholders: diff --git a/posthog/hogql/placeholders.py b/posthog/hogql/placeholders.py index 0399f6dc2d498..6d9eb5b017c4a 100644 --- a/posthog/hogql/placeholders.py +++ b/posthog/hogql/placeholders.py @@ -1,14 +1,14 @@ from typing import Dict from posthog.hogql import ast -from posthog.hogql.visitor import EverythingVisitor +from posthog.hogql.visitor import CloningVisitor def replace_placeholders(node: ast.Expr, placeholders: Dict[str, ast.Expr]) -> ast.Expr: return ReplacePlaceholders(placeholders).visit(node) -class ReplacePlaceholders(EverythingVisitor): +class ReplacePlaceholders(CloningVisitor): def __init__(self, placeholders: Dict[str, ast.Expr]): self.placeholders = placeholders @@ -22,6 +22,6 @@ def assert_no_placeholders(node: ast.Expr): AssertNoPlaceholders().visit(node) -class AssertNoPlaceholders(EverythingVisitor): +class AssertNoPlaceholders(CloningVisitor): def visit_placeholder(self, node): raise ValueError(f"Placeholder '{node.field}' not allowed in this context") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py new file mode 100644 index 0000000000000..649b7bb6df102 --- /dev/null +++ b/posthog/hogql/resolver.py @@ -0,0 +1,122 @@ +from typing import List, Optional + +from posthog.hogql import ast +from posthog.hogql.visitor import TraversingVisitor + + +def resolve_symbols(node: ast.SelectQuery): + Resolver().visit(node) + + +class ResolverException(ValueError): + pass + + +class Resolver(TraversingVisitor): + def __init__(self): + self.scopes: List[ast.SelectQuerySymbol] = [] + + def visit_alias(self, node: ast.Alias): + if node.symbol is not None: + return + + if len(self.scopes) == 0: + raise ResolverException("Aliases are allowed only within SELECT queries") + last_select = self.scopes[-1] + if node.alias in last_select.symbols: + raise ResolverException(f"Found multiple expressions with the same alias: {node.alias}") + if node.alias == "": + raise ResolverException("Alias cannot be empty") + + symbol = ast.AliasSymbol(name=node.alias, expr=node.expr) + last_select.symbols[node.alias] = symbol + self.visit(node.expr) + + def visit_field(self, node): + if node.symbol is not None: + return + if len(node.chain) == 0: + raise Exception("Invalid field access with empty chain") + + # resolve the first part of the chain + name = node.chain[0] + symbol: Optional[ast.Symbol] = None + for scope in reversed(self.scopes): + if name in scope.tables and len(node.chain) > 1: + symbol = scope.tables[name] + break + elif name in scope.symbols: + symbol = scope.symbols[name] + break + else: + fields_on_tables_in_scope = [table for table in scope.tables.values() if table.has_child(name)] + if len(fields_on_tables_in_scope) > 1: + raise ResolverException( + f"Found multiple joined tables with field \"{name}\": {', '.join([symbol.name for symbol in fields_on_tables_in_scope])}. Please specify which table you're selecting from." + ) + elif len(fields_on_tables_in_scope) == 1: + symbol = fields_on_tables_in_scope[0].get_child(name) + break + + if not symbol: + raise ResolverException(f'Cannot resolve symbol: "{name}"') + + # recursively resolve the rest of the chain + for name in node.chain[1:]: + symbol = symbol.get_child(name) + if symbol is None: + raise ResolverException(f"Cannot resolve symbol {', '.join(node.chain)}. Unable to resolve {name}") + + node.symbol = symbol + + def visit_join_expr(self, node): + if node.symbol is not None: + return + if len(self.scopes) == 0: + raise ResolverException("Unexpected JoinExpr outside a SELECT query") + last_select = self.scopes[-1] + if node.alias in last_select.tables: + raise ResolverException(f"Table alias with the same name as another table: {node.alias}") + + if isinstance(node.table, ast.Field): + if node.table.chain == ["events"]: + if node.alias is None: + node.alias = node.table.chain[0] + symbol = ast.TableSymbol(name=node.alias, table_name="events") + else: + raise ResolverException(f"Cannot resolve table {node.table.chain[0]}") + + elif isinstance(node.table, ast.SelectQuery): + symbol = self.visit(node.table) + symbol.name = node.alias + + else: + raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") + + node.table.symbol = symbol + last_select.tables[node.alias] = symbol + + self.visit(node.join_expr) + + def visit_select_query(self, node): + if node.symbol is not None: + return + + node.symbol = ast.SelectQuerySymbol(name="", symbols={}, tables={}) + self.scopes.append(node.symbol) + + if node.select_from: + self.visit(node.select_from) + if node.select: + for expr in node.select: + self.visit(expr) + if node.where: + self.visit(node.where) + if node.prewhere: + self.visit(node.prewhere) + if node.having: + self.visit(node.having) + + self.scopes.pop() + + return node.symbol diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index 55d943a1d197e..0313923c96c18 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -443,6 +443,13 @@ def test_select_from(self): select_from=ast.JoinExpr(table=ast.Field(chain=["events"]), alias="e"), ), ) + self.assertEqual( + parse_select("select 1 from events e"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["events"]), alias="e"), + ), + ) self.assertEqual( parse_select("select 1 from complex.table"), ast.SelectQuery( @@ -457,6 +464,13 @@ def test_select_from(self): select_from=ast.JoinExpr(table=ast.Field(chain=["complex", "table"]), alias="a"), ), ) + self.assertEqual( + parse_select("select 1 from complex.table a"), + ast.SelectQuery( + select=[ast.Constant(value=1)], + select_from=ast.JoinExpr(table=ast.Field(chain=["complex", "table"]), alias="a"), + ), + ) self.assertEqual( parse_select("select 1 from (select 1 from events)"), ast.SelectQuery( diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py new file mode 100644 index 0000000000000..19f05523a698d --- /dev/null +++ b/posthog/hogql/test/test_resolver.py @@ -0,0 +1,79 @@ +from posthog.hogql import ast +from posthog.hogql.parser import parse_select +from posthog.hogql.resolver import resolve_symbols +from posthog.test.base import BaseTest + + +class TestResolver(BaseTest): + def test_resolve_events_table(self): + expr = parse_select("SELECT event, events.timestamp FROM events WHERE events.event = 'test'") + resolve_symbols(expr) + + events_table_symbol = ast.TableSymbol(name="events", table_name="events") + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + select_query_symbol = ast.SelectQuerySymbol( + name="", + symbols={}, + tables={"events": events_table_symbol}, + ) + + self.assertEqual( + expr, + ast.SelectQuery( + select=[ + ast.Field(chain=["event"], symbol=event_field_symbol), + ast.Field(chain=["events", "timestamp"], symbol=timestamp_field_symbol), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="events", + ), + where=ast.CompareOperation( + left=ast.Field(chain=["events", "event"], symbol=event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), + ), + symbol=select_query_symbol, + ), + ) + + def test_resolve_events_table_alias(self): + expr = parse_select("SELECT event, e.timestamp FROM events e WHERE e.event = 'test'") + resolve_symbols(expr) + + events_table_symbol = ast.TableSymbol(name="e", table_name="events") + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + select_query_symbol = ast.SelectQuerySymbol( + name="", + symbols={}, + tables={"e": events_table_symbol}, + ) + + self.assertEqual( + expr, + ast.SelectQuery( + select=[ + ast.Field(chain=["event"], symbol=event_field_symbol), + ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="e", + ), + where=ast.CompareOperation( + left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), + ), + symbol=select_query_symbol, + ), + ) + + +# "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" +# "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" + + +# "select a, b, e.c from (select 1 as a, 2 as b, 3 as c) as e" -> 1, 2, 3 diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index 738b4ac3aed12..eedf80e367fc5 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -1,10 +1,10 @@ from posthog.hogql import ast from posthog.hogql.parser import parse_expr -from posthog.hogql.visitor import EverythingVisitor +from posthog.hogql.visitor import CloningVisitor from posthog.test.base import BaseTest -class ConstantVisitor(EverythingVisitor): +class ConstantVisitor(CloningVisitor): def __init__(self): self.constants = [] self.fields = [] @@ -93,4 +93,4 @@ def test_everything_visitor(self): ), ] ) - self.assertEqual(node, EverythingVisitor().visit(node)) + self.assertEqual(node, CloningVisitor().visit(node)) diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 66365f368bbc1..fce103f70e28f 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -8,7 +8,75 @@ def visit(self, node: ast.AST): return node.accept(self) -class EverythingVisitor(Visitor): +class TraversingVisitor(Visitor): + """Visitor that traverses the AST tree without returning anything""" + + def visit_expr(self, node: ast.Expr): + raise ValueError("Can not visit generic Expr node") + + def visit_alias(self, node: ast.Alias): + self.visit(node.expr) + + def visit_binary_operation(self, node: ast.BinaryOperation): + self.visit(node.left) + self.visit(node.right) + + def visit_and(self, node: ast.And): + for expr in node.exprs: + self.visit(expr) + + def visit_or(self, node: ast.Or): + for expr in node.exprs: + self.visit(expr) + + def visit_compare_operation(self, node: ast.CompareOperation): + self.visit(node.left) + self.visit(node.right) + + def visit_not(self, node: ast.Not): + self.visit(node.expr) + + def visit_order_expr(self, node: ast.OrderExpr): + self.visit(node.expr) + + def visit_constant(self, node: ast.Constant): + pass + + def visit_field(self, node: ast.Field): + pass + + def visit_placeholder(self, node: ast.Placeholder): + pass + + def visit_call(self, node: ast.Call): + for expr in node.args: + self.visit(expr) + + def visit_join_expr(self, node: ast.JoinExpr): + self.visit(node.table) + self.visit(node.join_expr) + self.visit(node.join_constraint) + + def visit_select_query(self, node: ast.SelectQuery): + self.visit(node.select_from) + for expr in node.select or []: + self.visit(expr) + self.visit(node.where) + self.visit(node.prewhere) + self.visit(node.having) + for expr in node.group_by or []: + self.visit(expr) + for expr in node.order_by or []: + self.visit(expr) + for expr in node.limit_by or []: + self.visit(expr) + self.visit(node.limit), + self.visit(node.offset), + + +class CloningVisitor(Visitor): + """Visitor that traverses and clones the AST tree""" + def visit_expr(self, node: ast.Expr): raise ValueError("Can not visit generic Expr node") @@ -56,10 +124,10 @@ def visit_field(self, node: ast.Field): def visit_placeholder(self, node: ast.Placeholder): return node - def visit_call(self, call: ast.Call): + def visit_call(self, node: ast.Call): return ast.Call( - name=call.name, - args=[self.visit(arg) for arg in call.args], + name=node.name, + args=[self.visit(arg) for arg in node.args], ) def visit_join_expr(self, node: ast.JoinExpr): From 62dd464a6a5f6016c9a6786b2ad6357e4173c0e5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 11:55:31 +0100 Subject: [PATCH 010/142] resolve aliases --- posthog/hogql/ast.py | 20 ++-- posthog/hogql/resolver.py | 6 +- posthog/hogql/test/test_resolver.py | 149 ++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+), 12 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 76cea55d685ad..0226bc3538e11 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -33,13 +33,7 @@ def has_child(self, name: str) -> bool: class AliasSymbol(Symbol): - expr: "Expr" - - def get_child(self, name: str) -> "Symbol": - if isinstance(self.expr, SelectQuery): - return self.expr.symbol.get_child(name) - elif isinstance(self.expr, Field): - return self.expr.symbol.get_child(name) + symbol: "Symbol" class TableSymbol(Symbol): @@ -60,11 +54,18 @@ def get_child(self, name: str) -> "Symbol": class SelectQuerySymbol(Symbol): - # expr: "Expr" - symbols: Dict[str, Symbol] tables: Dict[str, Symbol] + def get_child(self, name: str) -> "Symbol": + if name in self.symbols: + return self.symbols[name] + if name in self.tables: + return self.tables[name] + + def has_child(self, name: str) -> bool: + return name in self.symbols or name in self.tables + class FieldSymbol(Symbol): table: TableSymbol @@ -77,7 +78,6 @@ def get_child(self, name: str) -> "Symbol": raise NotImplementedError(f"Can not resolve field {self.name} on table events") else: raise NotImplementedError(f"Can not resolve fields on table: {self.name}") - self.table.get_child(name) class PropertySymbol(Symbol): diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 649b7bb6df102..d3ee3d71ef2fa 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -28,10 +28,11 @@ def visit_alias(self, node: ast.Alias): if node.alias == "": raise ResolverException("Alias cannot be empty") - symbol = ast.AliasSymbol(name=node.alias, expr=node.expr) - last_select.symbols[node.alias] = symbol self.visit(node.expr) + node.symbol = ast.AliasSymbol(name=node.alias, symbol=node.expr.symbol) + last_select.symbols[node.alias] = node.symbol + def visit_field(self, node): if node.symbol is not None: return @@ -43,6 +44,7 @@ def visit_field(self, node): symbol: Optional[ast.Symbol] = None for scope in reversed(self.scopes): if name in scope.tables and len(node.chain) > 1: + # CH assumes you're selecting a field, unless it's with a "." in the field, then check for tables symbol = scope.tables[name] break elif name in scope.symbols: diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 19f05523a698d..7153d42f18639 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -71,6 +71,155 @@ def test_resolve_events_table_alias(self): ), ) + def test_resolve_events_table_column_alias(self): + expr = parse_select("SELECT event as ee, ee, ee as e, e.timestamp FROM events e WHERE e.event = 'test'") + resolve_symbols(expr) + + events_table_symbol = ast.TableSymbol(name="e", table_name="events") + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + select_query_symbol = ast.SelectQuerySymbol( + name="", + symbols={ + "ee": ast.AliasSymbol(name="ee", symbol=event_field_symbol), + "e": ast.AliasSymbol(name="e", symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + }, + tables={"e": events_table_symbol}, + ) + + expected_query = ast.SelectQuery( + select=[ + ast.Alias( + alias="ee", + expr=ast.Field(chain=["event"], symbol=event_field_symbol), + symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol), + ), + ast.Field(chain=["ee"], symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + ast.Alias( + alias="e", + expr=ast.Field(chain=["ee"], symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + symbol=ast.AliasSymbol(name="e", symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + ), + ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="e", + ), + where=ast.CompareOperation( + left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), + ), + symbol=select_query_symbol, + ) + # asserting individually to help debug if something is off + self.assertEqual(expr.select, expected_query.select) + self.assertEqual(expr.select_from, expected_query.select_from) + self.assertEqual(expr.where, expected_query.where) + self.assertEqual(expr.symbol, expected_query.symbol) + self.assertEqual(expr, expected_query) + + def test_resolve_events_table_column_alias_inside_subquery(self): + expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(name="events", table_name="events") + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + expected_query = ast.SelectQuery( + select=[ + ast.Field( + chain=["b"], + symbol=ast.AliasSymbol( + name="b", + symbol=event_field_symbol, + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.SelectQuery( + select=[ + ast.Alias( + alias="b", + expr=ast.Field(chain=["event"], symbol=event_field_symbol), + symbol=ast.AliasSymbol( + name="b", + symbol=event_field_symbol, + ), + ), + ast.Alias( + alias="c", + expr=ast.Field(chain=["timestamp"], symbol=timestamp_field_symbol), + symbol=ast.AliasSymbol( + name="c", + symbol=timestamp_field_symbol, + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="events", + ), + symbol=ast.SelectQuerySymbol( + name="e", + symbols={ + "b": ast.AliasSymbol( + name="b", + symbol=event_field_symbol, + ), + "c": ast.AliasSymbol( + name="c", + symbol=timestamp_field_symbol, + ), + }, + tables={ + "events": events_table_symbol, + }, + ), + ), + alias="e", + ), + where=ast.CompareOperation( + left=ast.Field( + chain=["e", "b"], + symbol=ast.AliasSymbol( + name="b", + symbol=event_field_symbol, + ), + ), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), + ), + symbol=ast.SelectQuerySymbol( + name="", + symbols={}, + tables={ + "e": ast.SelectQuerySymbol( + name="e", + symbols={ + "b": ast.AliasSymbol( + name="b", + symbol=event_field_symbol, + ), + "c": ast.AliasSymbol( + name="c", + symbol=timestamp_field_symbol, + ), + }, + tables={ + "events": events_table_symbol, + }, + ) + }, + ), + ) + # asserting individually to help debug if something is off + self.assertEqual(expr.select, expected_query.select) + self.assertEqual(expr.select_from, expected_query.select_from) + self.assertEqual(expr.where, expected_query.where) + self.assertEqual(expr.symbol, expected_query.symbol) + self.assertEqual(expr, expected_query) + # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" # "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" From 6c04d20f53cc9e2e65549cb4ab5642a7c1f403e6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 14:19:20 +0100 Subject: [PATCH 011/142] refactor column and table aliases --- posthog/hogql/ast.py | 28 ++- posthog/hogql/parser.py | 2 +- posthog/hogql/resolver.py | 28 +-- posthog/hogql/test/test_resolver.py | 265 +++++++++++++++++++--------- 4 files changed, 223 insertions(+), 100 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 0226bc3538e11..66b8b5b12ed89 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -23,8 +23,6 @@ def accept(self, visitor): class Symbol(AST): - name: str - def get_child(self, name: str) -> "Symbol": raise NotImplementedError() @@ -32,9 +30,27 @@ def has_child(self, name: str) -> bool: return self.get_child(name) is not None -class AliasSymbol(Symbol): +class ColumnAliasSymbol(Symbol): + name: str symbol: "Symbol" + def get_child(self, name: str) -> "Symbol": + return self.symbol.get_child(name) + + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) + + +class TableAliasSymbol(Symbol): + name: str + symbol: "Symbol" + + def get_child(self, name: str) -> "Symbol": + return self.symbol.get_child(name) + + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) + class TableSymbol(Symbol): table_name: Literal["events"] @@ -68,6 +84,7 @@ def has_child(self, name: str) -> bool: class FieldSymbol(Symbol): + name: str table: TableSymbol def get_child(self, name: str) -> "Symbol": @@ -81,6 +98,7 @@ def get_child(self, name: str) -> "Symbol": class PropertySymbol(Symbol): + name: str field: FieldSymbol @@ -88,8 +106,8 @@ class Expr(AST): symbol: Optional[Symbol] -Symbol.update_forward_refs(Expr=Expr) -AliasSymbol.update_forward_refs(Expr=Expr) +ColumnAliasSymbol.update_forward_refs(Expr=Expr) +TableAliasSymbol.update_forward_refs(Expr=Expr) SelectQuerySymbol.update_forward_refs(Expr=Expr) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index 0f2424bb6d9cb..2266073968cf0 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -303,7 +303,7 @@ def visitColumnsExprAsterisk(self, ctx: HogQLParser.ColumnsExprAsteriskContext): return ast.Field(chain=["*"]) def visitColumnsExprSubquery(self, ctx: HogQLParser.ColumnsExprSubqueryContext): - raise NotImplementedError(f"Unsupported node: ColumnsExprSubquery") + return self.visit(ctx.selectUnionStmt()) def visitColumnsExprColumn(self, ctx: HogQLParser.ColumnsExprColumnContext): return self.visit(ctx.columnExpr()) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index d3ee3d71ef2fa..28d150022f22f 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -3,6 +3,8 @@ from posthog.hogql import ast from posthog.hogql.visitor import TraversingVisitor +# https://github.com/ClickHouse/ClickHouse/issues/23194 - "Describe how identifiers in SELECT queries are resolved" + def resolve_symbols(node: ast.SelectQuery): Resolver().visit(node) @@ -30,7 +32,7 @@ def visit_alias(self, node: ast.Alias): self.visit(node.expr) - node.symbol = ast.AliasSymbol(name=node.alias, symbol=node.expr.symbol) + node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=node.expr.symbol) last_select.symbols[node.alias] = node.symbol def visit_field(self, node): @@ -77,26 +79,30 @@ def visit_join_expr(self, node): if len(self.scopes) == 0: raise ResolverException("Unexpected JoinExpr outside a SELECT query") last_select = self.scopes[-1] - if node.alias in last_select.tables: - raise ResolverException(f"Table alias with the same name as another table: {node.alias}") if isinstance(node.table, ast.Field): + if node.alias is None: + node.alias = node.table.chain[0] + if node.alias in last_select.tables: + raise ResolverException(f"Table alias with the same name as another table: {node.alias}") + if node.table.chain == ["events"]: - if node.alias is None: - node.alias = node.table.chain[0] - symbol = ast.TableSymbol(name=node.alias, table_name="events") + node.table.symbol = ast.TableSymbol(table_name="events") + node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) else: raise ResolverException(f"Cannot resolve table {node.table.chain[0]}") elif isinstance(node.table, ast.SelectQuery): - symbol = self.visit(node.table) - symbol.name = node.alias + node.table.symbol = self.visit(node.table) + if node.alias is None: + node.symbol = node.table.symbol + else: + node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") - node.table.symbol = symbol - last_select.tables[node.alias] = symbol + last_select.tables[node.alias] = node.table.symbol self.visit(node.join_expr) @@ -104,7 +110,7 @@ def visit_select_query(self, node): if node.symbol is not None: return - node.symbol = ast.SelectQuerySymbol(name="", symbols={}, tables={}) + node.symbol = ast.SelectQuerySymbol(symbols={}, tables={}) self.scopes.append(node.symbol) if node.select_from: diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 7153d42f18639..8ae06e6b8a2c3 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -9,102 +9,114 @@ def test_resolve_events_table(self): expr = parse_select("SELECT event, events.timestamp FROM events WHERE events.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(name="events", table_name="events") + events_table_symbol = ast.TableSymbol(table_name="events") event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - name="", symbols={}, tables={"events": events_table_symbol}, ) - self.assertEqual( - expr, - ast.SelectQuery( - select=[ - ast.Field(chain=["event"], symbol=event_field_symbol), - ast.Field(chain=["events", "timestamp"], symbol=timestamp_field_symbol), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=events_table_symbol), - alias="events", - ), - where=ast.CompareOperation( - left=ast.Field(chain=["events", "event"], symbol=event_field_symbol), - op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), - ), - symbol=select_query_symbol, + expected = ast.SelectQuery( + select=[ + ast.Field(chain=["event"], symbol=event_field_symbol), + ast.Field(chain=["events", "timestamp"], symbol=timestamp_field_symbol), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="events", + symbol=ast.TableAliasSymbol(name="events", symbol=events_table_symbol), ), + where=ast.CompareOperation( + left=ast.Field(chain=["events", "event"], symbol=event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), + ), + symbol=select_query_symbol, ) + # asserting individually to help debug if something is off + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + def test_resolve_events_table_alias(self): expr = parse_select("SELECT event, e.timestamp FROM events e WHERE e.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(name="e", table_name="events") + events_table_symbol = ast.TableSymbol(table_name="events") event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - name="", symbols={}, tables={"e": events_table_symbol}, ) - self.assertEqual( - expr, - ast.SelectQuery( - select=[ - ast.Field(chain=["event"], symbol=event_field_symbol), - ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=events_table_symbol), - alias="e", - ), - where=ast.CompareOperation( - left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), - op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), - ), - symbol=select_query_symbol, + expected = ast.SelectQuery( + select=[ + ast.Field(chain=["event"], symbol=event_field_symbol), + ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="e", + symbol=ast.TableAliasSymbol(name="e", symbol=events_table_symbol), + ), + where=ast.CompareOperation( + left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="test"), ), + symbol=select_query_symbol, ) + # asserting individually to help debug if something is off + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + def test_resolve_events_table_column_alias(self): expr = parse_select("SELECT event as ee, ee, ee as e, e.timestamp FROM events e WHERE e.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(name="e", table_name="events") + events_table_symbol = ast.TableSymbol(table_name="events") event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - name="", symbols={ - "ee": ast.AliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.AliasSymbol(name="e", symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), + "e": ast.ColumnAliasSymbol( + name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) + ), }, tables={"e": events_table_symbol}, ) - expected_query = ast.SelectQuery( + expected = ast.SelectQuery( select=[ ast.Alias( alias="ee", expr=ast.Field(chain=["event"], symbol=event_field_symbol), - symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol), + symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), ), - ast.Field(chain=["ee"], symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + ast.Field(chain=["ee"], symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol)), ast.Alias( alias="e", - expr=ast.Field(chain=["ee"], symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), - symbol=ast.AliasSymbol(name="e", symbol=ast.AliasSymbol(name="ee", symbol=event_field_symbol)), + expr=ast.Field(chain=["ee"], symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol)), + symbol=ast.ColumnAliasSymbol( + name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) + ), ), ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), ], select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="e", + symbol=ast.TableAliasSymbol(name="e", symbol=events_table_symbol), ), where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), @@ -114,23 +126,38 @@ def test_resolve_events_table_column_alias(self): symbol=select_query_symbol, ) # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected_query.select) - self.assertEqual(expr.select_from, expected_query.select_from) - self.assertEqual(expr.where, expected_query.where) - self.assertEqual(expr.symbol, expected_query.symbol) - self.assertEqual(expr, expected_query) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) def test_resolve_events_table_column_alias_inside_subquery(self): expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(name="events", table_name="events") + events_table_symbol = ast.TableSymbol(table_name="events") event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) - expected_query = ast.SelectQuery( + inner_select_symbol = ast.SelectQuerySymbol( + symbols={ + "b": ast.ColumnAliasSymbol( + name="b", + symbol=event_field_symbol, + ), + "c": ast.ColumnAliasSymbol( + name="c", + symbol=timestamp_field_symbol, + ), + }, + tables={ + "events": events_table_symbol, + }, + ) + expected = ast.SelectQuery( select=[ ast.Field( chain=["b"], - symbol=ast.AliasSymbol( + symbol=ast.ColumnAliasSymbol( name="b", symbol=event_field_symbol, ), @@ -142,7 +169,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): ast.Alias( alias="b", expr=ast.Field(chain=["event"], symbol=event_field_symbol), - symbol=ast.AliasSymbol( + symbol=ast.ColumnAliasSymbol( name="b", symbol=event_field_symbol, ), @@ -150,7 +177,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): ast.Alias( alias="c", expr=ast.Field(chain=["timestamp"], symbol=timestamp_field_symbol), - symbol=ast.AliasSymbol( + symbol=ast.ColumnAliasSymbol( name="c", symbol=timestamp_field_symbol, ), @@ -159,30 +186,17 @@ def test_resolve_events_table_column_alias_inside_subquery(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="events", + symbol=ast.ColumnAliasSymbol(name="events", symbol=events_table_symbol), ), - symbol=ast.SelectQuerySymbol( - name="e", - symbols={ - "b": ast.AliasSymbol( - name="b", - symbol=event_field_symbol, - ), - "c": ast.AliasSymbol( - name="c", - symbol=timestamp_field_symbol, - ), - }, - tables={ - "events": events_table_symbol, - }, - ), + symbol=inner_select_symbol, ), alias="e", + symbol=ast.TableAliasSymbol(name="e", symbol=inner_select_symbol), ), where=ast.CompareOperation( left=ast.Field( chain=["e", "b"], - symbol=ast.AliasSymbol( + symbol=ast.ColumnAliasSymbol( name="b", symbol=event_field_symbol, ), @@ -191,17 +205,15 @@ def test_resolve_events_table_column_alias_inside_subquery(self): right=ast.Constant(value="test"), ), symbol=ast.SelectQuerySymbol( - name="", symbols={}, tables={ "e": ast.SelectQuerySymbol( - name="e", symbols={ - "b": ast.AliasSymbol( + "b": ast.ColumnAliasSymbol( name="b", symbol=event_field_symbol, ), - "c": ast.AliasSymbol( + "c": ast.ColumnAliasSymbol( name="c", symbol=timestamp_field_symbol, ), @@ -214,11 +226,86 @@ def test_resolve_events_table_column_alias_inside_subquery(self): ), ) # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected_query.select) - self.assertEqual(expr.select_from, expected_query.select_from) - self.assertEqual(expr.where, expected_query.where) - self.assertEqual(expr.symbol, expected_query.symbol) - self.assertEqual(expr, expected_query) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + + def test_resolve_standard_subquery(self): + expr = parse_select( + "SELECT event, (select count() from events where event = e.event) as c FROM events e where event = '$pageview'" + ) + resolve_symbols(expr) + + outer_events_table_symbol = ast.TableSymbol(table_name="events") + outer_event_field_symbol = ast.FieldSymbol(name="event", table=outer_events_table_symbol) + + inner_events_table_symbol = ast.TableSymbol(table_name="events") + inner_event_field_symbol = ast.FieldSymbol(name="event", table=inner_events_table_symbol) + + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=outer_event_field_symbol, + ), + ast.Alias( + alias="c", + expr=ast.SelectQuery( + select=[ast.Call(name="count", args=[])], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=inner_events_table_symbol), + alias="events", + symbol=ast.ColumnAliasSymbol(name="events", symbol=inner_events_table_symbol), + ), + symbol=ast.SelectQuerySymbol( + symbols={}, + tables={"events": inner_events_table_symbol}, + ), + where=ast.CompareOperation( + left=ast.Field(chain=["event"], symbol=inner_event_field_symbol), + op=ast.CompareOperationType.Eq, + right=ast.Field(chain=["e", "event"], symbol=outer_event_field_symbol), + ), + ), + symbol=ast.ColumnAliasSymbol( + name="c", + symbol=ast.SelectQuerySymbol( + symbols={}, + tables={"events": inner_events_table_symbol}, + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=outer_events_table_symbol), + alias="e", + symbol=ast.ColumnAliasSymbol(name="e", symbol=outer_events_table_symbol), + ), + where=ast.CompareOperation( + left=ast.Field( + chain=["event"], + symbol=outer_event_field_symbol, + ), + op=ast.CompareOperationType.Eq, + right=ast.Constant(value="$pageview"), + ), + symbol=ast.SelectQuerySymbol( + symbols={ + "c": ast.ColumnAliasSymbol( + name="c", symbol=ast.SelectQuerySymbol(symbols={}, tables={"events": inner_events_table_symbol}) + ) + }, + tables={"e": outer_events_table_symbol}, + ), + ) + # asserting individually to help debug if something is off + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" @@ -226,3 +313,15 @@ def test_resolve_events_table_column_alias_inside_subquery(self): # "select a, b, e.c from (select 1 as a, 2 as b, 3 as c) as e" -> 1, 2, 3 + +# # good +# SELECT t.x FROM (SELECT 1 AS x) AS t; +# SELECT t.x FROM (SELECT x FROM tbl) AS t; +# SELECT x FROM (SELECT x FROM tbl) AS t; + +# # bad +# SELECT x, (SELECT 1 AS x); -- does not work, `x` is not visible; +# SELECT x IN (SELECT 1 AS x); -- does not work either; +# SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x); -- this will work, but keep in mind that there are two different `x`. +# SELECT tbl.x FROM (SELECT x FROM tbl) AS t; -- this is wrong, the `tbl` name is not exported +# SELECT t2.x FROM (SELECT x FROM tbl AS t2) AS t; -- this is also wrong, the `t2` alias is not exported From c5501472e94fecf57158453a8d0d8471912c7117 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 21:26:04 +0100 Subject: [PATCH 012/142] column resolver --- posthog/hogql/ast.py | 16 +-- posthog/hogql/resolver.py | 68 ++++++++---- posthog/hogql/test/test_resolver.py | 156 ++++++++-------------------- 3 files changed, 96 insertions(+), 144 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 66b8b5b12ed89..83b8974ac0325 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -59,28 +59,32 @@ def has_child(self, name: str) -> bool: if self.table_name == "events": return name in EVENT_FIELDS else: - raise NotImplementedError(f"Can not resolve table: {self.name}") + raise NotImplementedError(f"Can not resolve table: {self.table_name}") def get_child(self, name: str) -> "Symbol": if self.table_name == "events": if name in EVENT_FIELDS: return FieldSymbol(name=name, table=self) else: - raise NotImplementedError(f"Can not resolve table: {self.name}") + raise NotImplementedError(f"Can not resolve table: {self.table_name}") class SelectQuerySymbol(Symbol): - symbols: Dict[str, Symbol] + # all aliases a select query has access to in its scope + aliases: Dict[str, Symbol] + # all symbols a select query exports + columns: Dict[str, Symbol] + # all tables we join in this query on which we look for aliases tables: Dict[str, Symbol] def get_child(self, name: str) -> "Symbol": - if name in self.symbols: - return self.symbols[name] + if name in self.columns: + return self.columns[name] if name in self.tables: return self.tables[name] def has_child(self, name: str) -> bool: - return name in self.symbols or name in self.tables + return name in self.columns or name in self.tables class FieldSymbol(Symbol): diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 28d150022f22f..8765448675d33 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -25,15 +25,15 @@ def visit_alias(self, node: ast.Alias): if len(self.scopes) == 0: raise ResolverException("Aliases are allowed only within SELECT queries") last_select = self.scopes[-1] - if node.alias in last_select.symbols: + if node.alias in last_select.aliases: raise ResolverException(f"Found multiple expressions with the same alias: {node.alias}") if node.alias == "": raise ResolverException("Alias cannot be empty") self.visit(node.expr) - node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=node.expr.symbol) - last_select.symbols[node.alias] = node.symbol + node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=unwrap_column_alias_symbol(node.expr.symbol)) + last_select.aliases[node.alias] = node.symbol def visit_field(self, node): if node.symbol is not None: @@ -44,23 +44,23 @@ def visit_field(self, node): # resolve the first part of the chain name = node.chain[0] symbol: Optional[ast.Symbol] = None - for scope in reversed(self.scopes): - if name in scope.tables and len(node.chain) > 1: - # CH assumes you're selecting a field, unless it's with a "." in the field, then check for tables - symbol = scope.tables[name] - break - elif name in scope.symbols: - symbol = scope.symbols[name] - break - else: - fields_on_tables_in_scope = [table for table in scope.tables.values() if table.has_child(name)] - if len(fields_on_tables_in_scope) > 1: - raise ResolverException( - f"Found multiple joined tables with field \"{name}\": {', '.join([symbol.name for symbol in fields_on_tables_in_scope])}. Please specify which table you're selecting from." - ) - elif len(fields_on_tables_in_scope) == 1: - symbol = fields_on_tables_in_scope[0].get_child(name) - break + + # to keep things simple, we only allow selecting fields from within this (select x) scope + scope = self.scopes[-1] + + if len(node.chain) > 1 and name in scope.tables: + # CH assumes you're selecting a field, unless it's with a "." in the field, then check for tables + symbol = scope.tables[name] + elif name in scope.aliases: + symbol = scope.aliases[name] + else: + fields_on_tables_in_scope = [table for table in scope.tables.values() if table.has_child(name)] + if len(fields_on_tables_in_scope) > 1: + raise ResolverException( + f'Found multiple joined tables with field "{name}". Please where you\'re selecting from.' + ) + elif len(fields_on_tables_in_scope) == 1: + symbol = fields_on_tables_in_scope[0].get_child(name) if not symbol: raise ResolverException(f'Cannot resolve symbol: "{name}"') @@ -88,7 +88,10 @@ def visit_join_expr(self, node): if node.table.chain == ["events"]: node.table.symbol = ast.TableSymbol(table_name="events") - node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) + if node.alias == node.table.symbol.table_name: + node.symbol = node.table.symbol + else: + node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) else: raise ResolverException(f"Cannot resolve table {node.table.chain[0]}") @@ -102,7 +105,7 @@ def visit_join_expr(self, node): else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") - last_select.tables[node.alias] = node.table.symbol + last_select.tables[node.alias] = node.symbol self.visit(node.join_expr) @@ -110,7 +113,7 @@ def visit_select_query(self, node): if node.symbol is not None: return - node.symbol = ast.SelectQuerySymbol(symbols={}, tables={}) + node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}) self.scopes.append(node.symbol) if node.select_from: @@ -118,6 +121,15 @@ def visit_select_query(self, node): if node.select: for expr in node.select: self.visit(expr) + if isinstance(expr.symbol, ast.ColumnAliasSymbol): + node.symbol.columns[expr.symbol.name] = expr.symbol + + elif isinstance(expr, ast.Alias): + node.symbol.columns[expr.alias] = expr.symbol + + elif isinstance(expr.symbol, ast.FieldSymbol): + node.symbol.columns[expr.symbol.name] = expr.symbol + if node.where: self.visit(node.where) if node.prewhere: @@ -128,3 +140,13 @@ def visit_select_query(self, node): self.scopes.pop() return node.symbol + + +def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: + i = 0 + while isinstance(symbol, ast.ColumnAliasSymbol): + symbol = symbol.symbol + i += 1 + if i > 100: + raise ResolverException("ColumnAliasSymbol recursion too deep!") + return symbol diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 8ae06e6b8a2c3..30de26cb15346 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -1,6 +1,6 @@ from posthog.hogql import ast from posthog.hogql.parser import parse_select -from posthog.hogql.resolver import resolve_symbols +from posthog.hogql.resolver import ResolverException, resolve_symbols from posthog.test.base import BaseTest @@ -13,7 +13,8 @@ def test_resolve_events_table(self): event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - symbols={}, + aliases={}, + columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, tables={"events": events_table_symbol}, ) @@ -25,7 +26,7 @@ def test_resolve_events_table(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="events", - symbol=ast.TableAliasSymbol(name="events", symbol=events_table_symbol), + symbol=events_table_symbol, ), where=ast.CompareOperation( left=ast.Field(chain=["events", "event"], symbol=event_field_symbol), @@ -50,8 +51,9 @@ def test_resolve_events_table_alias(self): event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - symbols={}, - tables={"e": events_table_symbol}, + aliases={}, + columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, + tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, ) expected = ast.SelectQuery( @@ -86,14 +88,18 @@ def test_resolve_events_table_column_alias(self): events_table_symbol = ast.TableSymbol(table_name="events") event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + select_query_symbol = ast.SelectQuerySymbol( - symbols={ + aliases={ "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.ColumnAliasSymbol( - name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) - ), + "e": ast.ColumnAliasSymbol(name="e", symbol=event_field_symbol), + }, + columns={ + "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), + "e": ast.ColumnAliasSymbol(name="e", symbol=event_field_symbol), + "timestamp": timestamp_field_symbol, }, - tables={"e": events_table_symbol}, + tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, ) expected = ast.SelectQuery( @@ -101,22 +107,20 @@ def test_resolve_events_table_column_alias(self): ast.Alias( alias="ee", expr=ast.Field(chain=["event"], symbol=event_field_symbol), - symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), + symbol=select_query_symbol.aliases["ee"], ), - ast.Field(chain=["ee"], symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol)), + ast.Field(chain=["ee"], symbol=select_query_symbol.aliases["ee"]), ast.Alias( alias="e", - expr=ast.Field(chain=["ee"], symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol)), - symbol=ast.ColumnAliasSymbol( - name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) - ), + expr=ast.Field(chain=["ee"], symbol=select_query_symbol.aliases["ee"]), + symbol=select_query_symbol.aliases["e"], ), ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), ], select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="e", - symbol=ast.TableAliasSymbol(name="e", symbol=events_table_symbol), + symbol=select_query_symbol.tables["e"], ), where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), @@ -139,15 +143,13 @@ def test_resolve_events_table_column_alias_inside_subquery(self): event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) inner_select_symbol = ast.SelectQuerySymbol( - symbols={ - "b": ast.ColumnAliasSymbol( - name="b", - symbol=event_field_symbol, - ), - "c": ast.ColumnAliasSymbol( - name="c", - symbol=timestamp_field_symbol, - ), + aliases={ + "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), + "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), + }, + columns={ + "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), + "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), }, tables={ "events": events_table_symbol, @@ -186,7 +188,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="events", - symbol=ast.ColumnAliasSymbol(name="events", symbol=events_table_symbol), + symbol=events_table_symbol, ), symbol=inner_select_symbol, ), @@ -205,24 +207,11 @@ def test_resolve_events_table_column_alias_inside_subquery(self): right=ast.Constant(value="test"), ), symbol=ast.SelectQuerySymbol( - symbols={}, - tables={ - "e": ast.SelectQuerySymbol( - symbols={ - "b": ast.ColumnAliasSymbol( - name="b", - symbol=event_field_symbol, - ), - "c": ast.ColumnAliasSymbol( - name="c", - symbol=timestamp_field_symbol, - ), - }, - tables={ - "events": events_table_symbol, - }, - ) + aliases={}, + columns={ + "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), }, + tables={"e": ast.TableAliasSymbol(name="e", symbol=inner_select_symbol)}, ), ) # asserting individually to help debug if something is off @@ -232,80 +221,14 @@ def test_resolve_events_table_column_alias_inside_subquery(self): self.assertEqual(expr.symbol, expected.symbol) self.assertEqual(expr, expected) - def test_resolve_standard_subquery(self): + def test_resolve_subquery_no_field_access(self): + # "Aliases defined outside of subquery are not visible in subqueries (but see below)." expr = parse_select( "SELECT event, (select count() from events where event = e.event) as c FROM events e where event = '$pageview'" ) - resolve_symbols(expr) - - outer_events_table_symbol = ast.TableSymbol(table_name="events") - outer_event_field_symbol = ast.FieldSymbol(name="event", table=outer_events_table_symbol) - - inner_events_table_symbol = ast.TableSymbol(table_name="events") - inner_event_field_symbol = ast.FieldSymbol(name="event", table=inner_events_table_symbol) - - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - symbol=outer_event_field_symbol, - ), - ast.Alias( - alias="c", - expr=ast.SelectQuery( - select=[ast.Call(name="count", args=[])], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=inner_events_table_symbol), - alias="events", - symbol=ast.ColumnAliasSymbol(name="events", symbol=inner_events_table_symbol), - ), - symbol=ast.SelectQuerySymbol( - symbols={}, - tables={"events": inner_events_table_symbol}, - ), - where=ast.CompareOperation( - left=ast.Field(chain=["event"], symbol=inner_event_field_symbol), - op=ast.CompareOperationType.Eq, - right=ast.Field(chain=["e", "event"], symbol=outer_event_field_symbol), - ), - ), - symbol=ast.ColumnAliasSymbol( - name="c", - symbol=ast.SelectQuerySymbol( - symbols={}, - tables={"events": inner_events_table_symbol}, - ), - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=outer_events_table_symbol), - alias="e", - symbol=ast.ColumnAliasSymbol(name="e", symbol=outer_events_table_symbol), - ), - where=ast.CompareOperation( - left=ast.Field( - chain=["event"], - symbol=outer_event_field_symbol, - ), - op=ast.CompareOperationType.Eq, - right=ast.Constant(value="$pageview"), - ), - symbol=ast.SelectQuerySymbol( - symbols={ - "c": ast.ColumnAliasSymbol( - name="c", symbol=ast.SelectQuerySymbol(symbols={}, tables={"events": inner_events_table_symbol}) - ) - }, - tables={"e": outer_events_table_symbol}, - ), - ) - # asserting individually to help debug if something is off - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.symbol, expected.symbol) - self.assertEqual(expr, expected) + with self.assertRaises(ResolverException) as e: + resolve_symbols(expr) + self.assertEqual(str(e.exception), 'Cannot resolve symbol: "e"') # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" @@ -318,6 +241,9 @@ def test_resolve_standard_subquery(self): # SELECT t.x FROM (SELECT 1 AS x) AS t; # SELECT t.x FROM (SELECT x FROM tbl) AS t; # SELECT x FROM (SELECT x FROM tbl) AS t; +# SELECT 1 AS x, x, x + 1; +# SELECT x, x + 1, 1 AS x; +# SELECT x, 1 + (2 + (3 AS x)); # # bad # SELECT x, (SELECT 1 AS x); -- does not work, `x` is not visible; From 2253f411c3c42b03667bfe352d24ae43c6a375be Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 21:52:04 +0100 Subject: [PATCH 013/142] make sure some things error --- posthog/hogql/ast.py | 4 +--- posthog/hogql/resolver.py | 16 +++++++++------- posthog/hogql/test/test_resolver.py | 21 ++++++++++++++------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 83b8974ac0325..f3193d24ca6d3 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -80,11 +80,9 @@ class SelectQuerySymbol(Symbol): def get_child(self, name: str) -> "Symbol": if name in self.columns: return self.columns[name] - if name in self.tables: - return self.tables[name] def has_child(self, name: str) -> bool: - return name in self.columns or name in self.tables + return name in self.columns class FieldSymbol(Symbol): diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 8765448675d33..b2ae30415c1b1 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -54,22 +54,24 @@ def visit_field(self, node): elif name in scope.aliases: symbol = scope.aliases[name] else: - fields_on_tables_in_scope = [table for table in scope.tables.values() if table.has_child(name)] - if len(fields_on_tables_in_scope) > 1: + fields_in_scope = [table.get_child(name) for table in scope.tables.values() if table.has_child(name)] + if len(fields_in_scope) > 1: raise ResolverException( f'Found multiple joined tables with field "{name}". Please where you\'re selecting from.' ) - elif len(fields_on_tables_in_scope) == 1: - symbol = fields_on_tables_in_scope[0].get_child(name) + elif len(fields_in_scope) == 1: + symbol = fields_in_scope[0] if not symbol: raise ResolverException(f'Cannot resolve symbol: "{name}"') # recursively resolve the rest of the chain - for name in node.chain[1:]: - symbol = symbol.get_child(name) + for child_name in node.chain[1:]: + symbol = symbol.get_child(child_name) if symbol is None: - raise ResolverException(f"Cannot resolve symbol {', '.join(node.chain)}. Unable to resolve {name}") + raise ResolverException( + f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {child_name} on {name}" + ) node.symbol = symbol diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 30de26cb15346..7095b8b43b824 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -230,6 +230,20 @@ def test_resolve_subquery_no_field_access(self): resolve_symbols(expr) self.assertEqual(str(e.exception), 'Cannot resolve symbol: "e"') + def test_resolve_errors(self): + queries = [ + "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", + "SELECT x, (SELECT 1 AS x)", + "SELECT x IN (SELECT 1 AS x)", + "SELECT events.x FROM (SELECT event as x FROM events) AS t", + "SELECT x.y FROM (SELECT event as y FROM events AS x) AS t", + # "SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x)", + ] + for query in queries: + with self.assertRaises(ResolverException) as e: + resolve_symbols(parse_select(query)) + self.assertEqual(str(e.exception), "Cannot resolve symbol") + # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" # "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" @@ -244,10 +258,3 @@ def test_resolve_subquery_no_field_access(self): # SELECT 1 AS x, x, x + 1; # SELECT x, x + 1, 1 AS x; # SELECT x, 1 + (2 + (3 AS x)); - -# # bad -# SELECT x, (SELECT 1 AS x); -- does not work, `x` is not visible; -# SELECT x IN (SELECT 1 AS x); -- does not work either; -# SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x); -- this will work, but keep in mind that there are two different `x`. -# SELECT tbl.x FROM (SELECT x FROM tbl) AS t; -- this is wrong, the `tbl` name is not exported -# SELECT t2.x FROM (SELECT x FROM tbl AS t2) AS t; -- this is also wrong, the `t2` alias is not exported From a0068724724d63d59544651aad963b27d15e09b8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 22:57:53 +0100 Subject: [PATCH 014/142] annotate --- posthog/hogql/resolver.py | 162 +++++++++++++++++++++----------------- 1 file changed, 89 insertions(+), 73 deletions(-) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index b2ae30415c1b1..6482d0517c202 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -18,76 +18,61 @@ class Resolver(TraversingVisitor): def __init__(self): self.scopes: List[ast.SelectQuerySymbol] = [] - def visit_alias(self, node: ast.Alias): - if node.symbol is not None: - return - - if len(self.scopes) == 0: - raise ResolverException("Aliases are allowed only within SELECT queries") - last_select = self.scopes[-1] - if node.alias in last_select.aliases: - raise ResolverException(f"Found multiple expressions with the same alias: {node.alias}") - if node.alias == "": - raise ResolverException("Alias cannot be empty") - - self.visit(node.expr) - - node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=unwrap_column_alias_symbol(node.expr.symbol)) - last_select.aliases[node.alias] = node.symbol + def visit_select_query(self, node): + """Visit each SELECT query or subquery.""" - def visit_field(self, node): if node.symbol is not None: return - if len(node.chain) == 0: - raise Exception("Invalid field access with empty chain") - # resolve the first part of the chain - name = node.chain[0] - symbol: Optional[ast.Symbol] = None + # Create a new lexical scope each time we enter a SELECT query. + node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}) + # Keep those scopes stacked in a list as we traverse the tree. + self.scopes.append(node.symbol) - # to keep things simple, we only allow selecting fields from within this (select x) scope - scope = self.scopes[-1] + # Visit all the FROM and JOIN tables (JoinExpr nodes) + if node.select_from: + self.visit(node.select_from) - if len(node.chain) > 1 and name in scope.tables: - # CH assumes you're selecting a field, unless it's with a "." in the field, then check for tables - symbol = scope.tables[name] - elif name in scope.aliases: - symbol = scope.aliases[name] - else: - fields_in_scope = [table.get_child(name) for table in scope.tables.values() if table.has_child(name)] - if len(fields_in_scope) > 1: - raise ResolverException( - f'Found multiple joined tables with field "{name}". Please where you\'re selecting from.' - ) - elif len(fields_in_scope) == 1: - symbol = fields_in_scope[0] + # Visit all the SELECT columns. + # Then mark them for export in "columns". This means they will be available outside of this query via: + # SELECT e.event, e.timestamp from (SELECT event, timestamp FROM events) AS e + for expr in node.select or []: + self.visit(expr) + if isinstance(expr.symbol, ast.ColumnAliasSymbol): + node.symbol.columns[expr.symbol.name] = expr.symbol + elif isinstance(expr, ast.Alias): + node.symbol.columns[expr.alias] = expr.symbol + elif isinstance(expr.symbol, ast.FieldSymbol): + node.symbol.columns[expr.symbol.name] = expr.symbol - if not symbol: - raise ResolverException(f'Cannot resolve symbol: "{name}"') + if node.where: + self.visit(node.where) + if node.prewhere: + self.visit(node.prewhere) + if node.having: + self.visit(node.having) - # recursively resolve the rest of the chain - for child_name in node.chain[1:]: - symbol = symbol.get_child(child_name) - if symbol is None: - raise ResolverException( - f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {child_name} on {name}" - ) + self.scopes.pop() - node.symbol = symbol + return node.symbol def visit_join_expr(self, node): + """Visit each FROM and JOIN table or subquery.""" + if node.symbol is not None: return if len(self.scopes) == 0: raise ResolverException("Unexpected JoinExpr outside a SELECT query") - last_select = self.scopes[-1] + scope = self.scopes[-1] if isinstance(node.table, ast.Field): if node.alias is None: + # Make sure there is a way to call the field in the scope. node.alias = node.table.chain[0] - if node.alias in last_select.tables: - raise ResolverException(f"Table alias with the same name as another table: {node.alias}") + if node.alias in scope.tables: + raise ResolverException(f'Already have a joined table called "{node.alias}", can\'t redefine.') + # Only joining the events table is supported if node.table.chain == ["events"]: node.table.symbol = ast.TableSymbol(table_name="events") if node.alias == node.table.symbol.table_name: @@ -107,41 +92,72 @@ def visit_join_expr(self, node): else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") - last_select.tables[node.alias] = node.symbol + scope.tables[node.alias] = node.symbol self.visit(node.join_expr) - def visit_select_query(self, node): + def visit_alias(self, node: ast.Alias): + """Visit column aliases. SELECT 1, (select 3 as y) as x.""" if node.symbol is not None: return - node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}) - self.scopes.append(node.symbol) + if len(self.scopes) == 0: + raise ResolverException("Aliases are allowed only within SELECT queries") + scope = self.scopes[-1] + if node.alias in scope.aliases: + raise ResolverException(f"Cannot redefine an alias with the name: {node.alias}") + if node.alias == "": + raise ResolverException("Alias cannot be empty") - if node.select_from: - self.visit(node.select_from) - if node.select: - for expr in node.select: - self.visit(expr) - if isinstance(expr.symbol, ast.ColumnAliasSymbol): - node.symbol.columns[expr.symbol.name] = expr.symbol + self.visit(node.expr) + node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=unwrap_column_alias_symbol(node.expr.symbol)) + scope.aliases[node.alias] = node.symbol - elif isinstance(expr, ast.Alias): - node.symbol.columns[expr.alias] = expr.symbol + def visit_field(self, node): + """Visit a field such as ast.Field(chain=["e", "properties", "$browser"])""" + if node.symbol is not None: + return + if len(node.chain) == 0: + raise Exception("Invalid field access with empty chain") - elif isinstance(expr.symbol, ast.FieldSymbol): - node.symbol.columns[expr.symbol.name] = expr.symbol + # ClickHouse does not support subqueries accessing "x.event" like this: + # "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", + # + # But this is supported: + # "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t", + # + # Thus only look into the current scope, for columns and aliases. + scope = self.scopes[-1] + symbol: Optional[ast.Symbol] = None + name = node.chain[0] - if node.where: - self.visit(node.where) - if node.prewhere: - self.visit(node.prewhere) - if node.having: - self.visit(node.having) + if len(node.chain) > 1 and name in scope.tables: + # If the field has a chain of at least one (e.g "e", "event"), the first part could refer to a table. + symbol = scope.tables[name] + elif name in scope.columns: + symbol = scope.columns[name] + elif name in scope.aliases: + symbol = scope.aliases[name] + else: + # Look through all FROM/JOIN tables, if they export a field by this name. + fields_in_scope = [table.get_child(name) for table in scope.tables.values() if table.has_child(name)] + if len(fields_in_scope) > 1: + raise ResolverException(f'Ambiguous query. Found multiple sources for field "{name}".') + elif len(fields_in_scope) == 1: + symbol = fields_in_scope[0] - self.scopes.pop() + if not symbol: + raise ResolverException(f'Cannot resolve symbol: "{name}"') - return node.symbol + # Recursively resolve the rest of the chain until we can point to the deepest node. + for child_name in node.chain[1:]: + symbol = symbol.get_child(child_name) + if symbol is None: + raise ResolverException( + f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {child_name} on {name}" + ) + + node.symbol = symbol def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: From b7b5521b82968f58e3a55d4ed663a79b935ec3ff Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 9 Feb 2023 23:12:22 +0100 Subject: [PATCH 015/142] constants --- posthog/hogql/ast.py | 4 ++++ posthog/hogql/resolver.py | 6 ++++++ posthog/hogql/test/test_resolver.py | 14 ++++++-------- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index f3193d24ca6d3..5c001dbd80174 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -99,6 +99,10 @@ def get_child(self, name: str) -> "Symbol": raise NotImplementedError(f"Can not resolve fields on table: {self.name}") +class ConstantSymbol(Symbol): + value: Any + + class PropertySymbol(Symbol): name: str field: FieldSymbol diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 6482d0517c202..a3d92e598c72f 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -159,6 +159,12 @@ def visit_field(self, node): node.symbol = symbol + def visit_constant(self, node): + """Visit a constant""" + if node.symbol is not None: + return + node.symbol = ast.ConstantSymbol(value=node.value) + def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: i = 0 diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 7095b8b43b824..6b82b21672b2c 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -31,7 +31,7 @@ def test_resolve_events_table(self): where=ast.CompareOperation( left=ast.Field(chain=["events", "event"], symbol=event_field_symbol), op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), + right=ast.Constant(value="test", symbol=ast.ConstantSymbol(value="test")), ), symbol=select_query_symbol, ) @@ -69,7 +69,7 @@ def test_resolve_events_table_alias(self): where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), + right=ast.Constant(value="test", symbol=ast.ConstantSymbol(value="test")), ), symbol=select_query_symbol, ) @@ -125,7 +125,7 @@ def test_resolve_events_table_column_alias(self): where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), + right=ast.Constant(value="test", symbol=ast.ConstantSymbol(value="test")), ), symbol=select_query_symbol, ) @@ -204,7 +204,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): ), ), op=ast.CompareOperationType.Eq, - right=ast.Constant(value="test"), + right=ast.Constant(value="test", symbol=ast.ConstantSymbol(value="test")), ), symbol=ast.SelectQuerySymbol( aliases={}, @@ -237,18 +237,15 @@ def test_resolve_errors(self): "SELECT x IN (SELECT 1 AS x)", "SELECT events.x FROM (SELECT event as x FROM events) AS t", "SELECT x.y FROM (SELECT event as y FROM events AS x) AS t", - # "SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x)", ] for query in queries: with self.assertRaises(ResolverException) as e: resolve_symbols(parse_select(query)) - self.assertEqual(str(e.exception), "Cannot resolve symbol") + self.assertIn("Cannot resolve symbol", str(e.exception)) # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" # "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" - - # "select a, b, e.c from (select 1 as a, 2 as b, 3 as c) as e" -> 1, 2, 3 # # good @@ -258,3 +255,4 @@ def test_resolve_errors(self): # SELECT 1 AS x, x, x + 1; # SELECT x, x + 1, 1 AS x; # SELECT x, 1 + (2 + (3 AS x)); +# "SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x)", From 60376dd796377b51932b4bc10a386804104594a0 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 10 Feb 2023 00:01:43 +0100 Subject: [PATCH 016/142] simple sql query --- posthog/hogql/ast.py | 2 + posthog/hogql/query.py | 62 +++++++++++++++++++++++++++++ posthog/hogql/resolver.py | 2 + posthog/hogql/test/test_query.py | 24 +++++++++++ posthog/hogql/test/test_resolver.py | 2 +- 5 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 posthog/hogql/query.py create mode 100644 posthog/hogql/test/test_query.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 5c001dbd80174..fd2e252cb036c 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -65,6 +65,7 @@ def get_child(self, name: str) -> "Symbol": if self.table_name == "events": if name in EVENT_FIELDS: return FieldSymbol(name=name, table=self) + raise NotImplementedError(f"Event field not found: {name}") else: raise NotImplementedError(f"Can not resolve table: {self.table_name}") @@ -80,6 +81,7 @@ class SelectQuerySymbol(Symbol): def get_child(self, name: str) -> "Symbol": if name in self.columns: return self.columns[name] + raise NotImplementedError(f"Column not found: {name}") def has_child(self, name: str) -> bool: return name in self.columns diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py new file mode 100644 index 0000000000000..362f22dc6cef3 --- /dev/null +++ b/posthog/hogql/query.py @@ -0,0 +1,62 @@ +from typing import List, Optional, Union + +from pydantic import BaseModel, Extra + +from posthog.clickhouse.client.connection import Workload +from posthog.hogql import ast +from posthog.hogql.hogql import HogQLContext +from posthog.hogql.parser import parse_select +from posthog.hogql.printer import print_ast +from posthog.hogql.resolver import resolve_symbols +from posthog.models import Team +from posthog.queries.insight import insight_sync_execute + + +class HogQLQueryResponse(BaseModel): + class Config: + extra = Extra.forbid + + clickhouse: Optional[str] = None + columns: Optional[List] = None + hogql: Optional[str] = None + query: Optional[str] = None + results: Optional[List] = None + types: Optional[List] = None + + +def execute_hogql_query( + query: Union[str, ast.SelectQuery], + team: Team, + query_type: str = "hogql_query", + workload: Workload = Workload.ONLINE, +) -> HogQLQueryResponse: + if isinstance(query, ast.SelectQuery): + select_query = query + query = None + else: + select_query = parse_select(str(query), no_placeholders=True) + + if select_query.limit is None: + select_query.limit = ast.Constant(value=1000) + + hogql_context = HogQLContext(select_team_id=team.pk) + resolve_symbols(select_query) + clickhouse = print_ast(select_query, [], hogql_context, "clickhouse") + hogql = print_ast(select_query, [], hogql_context, "hogql") + + results, types = insight_sync_execute( + clickhouse, + hogql_context.values, + with_column_types=True, + query_type=query_type, + workload=workload, + ) + print_columns = [print_ast(col, [], HogQLContext(), "hogql") for col in select_query.select] + return HogQLQueryResponse( + query=query, + hogql=hogql, + clickhouse=clickhouse, + results=results, + columns=print_columns, + types=types, + ) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index a3d92e598c72f..67da8625e29c8 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -15,6 +15,8 @@ class ResolverException(ValueError): class Resolver(TraversingVisitor): + """The Resolver visits an AST and assigns Symbols to the nodes.""" + def __init__(self): self.scopes: List[ast.SelectQuerySymbol] = [] diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py new file mode 100644 index 0000000000000..1aec779f121ed --- /dev/null +++ b/posthog/hogql/test/test_query.py @@ -0,0 +1,24 @@ +from freezegun import freeze_time + +from posthog.hogql.query import execute_hogql_query +from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, flush_persons_and_events + + +class TestQuery(ClickhouseTestMixin, APIBaseTest): + def test_query(self): + with freeze_time("2020-01-10"): + _create_event( + distinct_id="bla", + event="random event", + team=self.team, + properties={"random_prop": "don't include", "some other prop": "with some text"}, + ) + _create_event( + distinct_id="bla", + event="random event", + team=self.team, + properties={"random_prop": "don't include", "some other prop": "with some text"}, + ) + flush_persons_and_events() + response = execute_hogql_query("select count(), event from events group by event", self.team) + self.assertEqual(response.results, [(2, "random event")]) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 6b82b21672b2c..bffe494cb1946 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -222,7 +222,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): self.assertEqual(expr, expected) def test_resolve_subquery_no_field_access(self): - # "Aliases defined outside of subquery are not visible in subqueries (but see below)." + # From ClickHouse's GitHub: "Aliases defined outside of subquery are not visible in subqueries (but see below)." expr = parse_select( "SELECT event, (select count() from events where event = e.event) as c FROM events e where event = '$pageview'" ) From 9683e9204e7c18d869e3fbebba93e2581ed3bf9a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Feb 2023 23:39:35 +0000 Subject: [PATCH 017/142] Update snapshots --- posthog/api/test/__snapshots__/test_element.ambr | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/posthog/api/test/__snapshots__/test_element.ambr b/posthog/api/test/__snapshots__/test_element.ambr index 024b0cd86835d..e01fd97dd1132 100644 --- a/posthog/api/test/__snapshots__/test_element.ambr +++ b/posthog/api/test/__snapshots__/test_element.ambr @@ -95,3 +95,14 @@ WHERE "posthog_organizationmembership"."user_id" = 2 /*controller='element-stats',route='api/element/stats/%3F%24'*/ ' --- +# name: TestElement.test_element_stats_postgres_queries_are_as_expected.3 + ' + SELECT "posthog_instancesetting"."id", + "posthog_instancesetting"."key", + "posthog_instancesetting"."raw_value" + FROM "posthog_instancesetting" + WHERE "posthog_instancesetting"."key" = 'constance:posthog:RATE_LIMIT_ENABLED' + ORDER BY "posthog_instancesetting"."id" ASC + LIMIT 1 /*controller='element-stats',route='api/element/stats/%3F%24'*/ + ' +--- From ac3048e9317965890cdad07455039b539945b8cf Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 10 Feb 2023 22:21:35 +0100 Subject: [PATCH 018/142] introduce "print name" --- posthog/hogql/ast.py | 2 ++ posthog/hogql/printer.py | 37 ++++++++++++++++++++++++++++--------- posthog/hogql/resolver.py | 25 ++++++++++++++++++++++--- 3 files changed, 52 insertions(+), 12 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index fd2e252cb036c..9064c3f6212b8 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -23,6 +23,8 @@ def accept(self, visitor): class Symbol(AST): + print_name: Optional[str] + def get_child(self, name: str) -> "Symbol": raise NotImplementedError() diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 65eb95b928eba..c61d1f82a8213 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -44,16 +44,28 @@ def print_ast( from_table = None if node.select_from: - if node.select_from.alias is not None: - raise ValueError("Table aliases not yet supported") - if isinstance(node.select_from.table, ast.Field): - if node.select_from.table.chain != ["events"]: - raise ValueError('Only selecting from the "events" table is supported') - from_table = "events" - elif isinstance(node.select_from.table, ast.SelectQuery): - from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + if node.symbol: + if isinstance(node.symbol, ast.TableSymbol): + if node.symbol.table_name != "events": + raise ValueError('Only selecting from the "events" table is supported') + from_table = f"events" + if node.symbol.print_name: + from_table = f"{from_table} AS {node.symbol.print_name}" + elif isinstance(node.symbol, ast.SelectQuerySymbol): + from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + if node.symbol.print_name: + from_table = f"{from_table} AS {node.symbol.print_name}" else: - raise ValueError("Only selecting from a table or a subquery is supported") + if node.select_from.alias is not None: + raise ValueError("Table aliases not yet supported") + if isinstance(node.select_from.table, ast.Field): + if node.select_from.table.chain != ["events"]: + raise ValueError('Only selecting from the "events" table is supported') + from_table = "events" + elif isinstance(node.select_from.table, ast.SelectQuery): + from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + else: + raise ValueError("Only selecting from a table or a subquery is supported") where = node.where # Guard with team_id if selecting from a table and printing ClickHouse SQL @@ -184,6 +196,13 @@ def print_ast( elif node.chain == ["person"]: query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" response = print_ast(parse_expr(query), stack, context, dialect) + elif node.symbol is not None: + if isinstance(node.symbol, ast.FieldSymbol): + response = f"{node.symbol.table.print_name}.{node.symbol.name}" + elif isinstance(node.symbol, ast.TableSymbol): + response = node.symbol.print_name + else: + raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") else: field_access = parse_field_access(node.chain, context) context.field_access_logs.append(field_access) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 67da8625e29c8..fb0ce78b41aef 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Dict, List, Optional from posthog.hogql import ast from posthog.hogql.visitor import TraversingVisitor @@ -19,6 +19,7 @@ class Resolver(TraversingVisitor): def __init__(self): self.scopes: List[ast.SelectQuerySymbol] = [] + self.global_tables: Dict[str, ast.Symbol] = {} def visit_select_query(self, node): """Visit each SELECT query or subquery.""" @@ -53,6 +54,14 @@ def visit_select_query(self, node): self.visit(node.prewhere) if node.having: self.visit(node.having) + for expr in node.group_by or []: + self.visit(expr) + for expr in node.order_by or []: + self.visit(expr) + for expr in node.limit_by or []: + self.visit(expr) + self.visit(node.limit) + self.visit(node.offset) self.scopes.pop() @@ -76,7 +85,9 @@ def visit_join_expr(self, node): # Only joining the events table is supported if node.table.chain == ["events"]: - node.table.symbol = ast.TableSymbol(table_name="events") + print_name = f"{node.alias[0:1] or 't'}{len(self.global_tables)}" + node.table.symbol = ast.TableSymbol(table_name="events", print_name=print_name) + self.global_tables[print_name] = node.table.symbol if node.alias == node.table.symbol.table_name: node.symbol = node.table.symbol else: @@ -89,13 +100,18 @@ def visit_join_expr(self, node): if node.alias is None: node.symbol = node.table.symbol else: - node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) + print_name = self._new_global_table_print_name(node.alias) + node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol, print_name=print_name) + self.global_tables[print_name] = node.symbol else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") scope.tables[node.alias] = node.symbol + # node.symbol.print_name = self._new_global_table_print_name(node.alias) + # self.global_tables[node.symbol.print_name] = node.symbol + self.visit(node.join_expr) def visit_alias(self, node: ast.Alias): @@ -167,6 +183,9 @@ def visit_constant(self, node): return node.symbol = ast.ConstantSymbol(value=node.value) + def _new_global_table_print_name(self, table_name): + return f"{table_name[0:1] or 't'}{len(self.global_tables)}" + def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: i = 0 From 86ddae59cff9350a8eff6881ed6adeac607cd8f8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 10 Feb 2023 22:42:25 +0100 Subject: [PATCH 019/142] visit_unknown --- posthog/hogql/ast.py | 8 +++-- posthog/hogql/test/test_visitor.py | 54 ++++++++++++++++++++---------- 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 9064c3f6212b8..014bf81de40f2 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -18,8 +18,12 @@ class Config: def accept(self, visitor): camel_case_name = camel_case_pattern.sub("_", self.__class__.__name__).lower() method_name = "visit_{}".format(camel_case_name) - visit = getattr(visitor, method_name) - return visit(self) + if hasattr(visitor, method_name): + visit = getattr(visitor, method_name) + return visit(self) + if hasattr(visitor, "visit_unknown"): + return visitor.visit_unknown(self) + raise ValueError("Visitor has no method visit_constant") class Symbol(AST): diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index eedf80e367fc5..0a67144f659cc 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -1,30 +1,29 @@ from posthog.hogql import ast from posthog.hogql.parser import parse_expr -from posthog.hogql.visitor import CloningVisitor +from posthog.hogql.visitor import CloningVisitor, Visitor from posthog.test.base import BaseTest -class ConstantVisitor(CloningVisitor): - def __init__(self): - self.constants = [] - self.fields = [] - self.operations = [] - - def visit_constant(self, node): - self.constants.append(node.value) - return super().visit_constant(node) +class TestVisitor(BaseTest): + def test_visitor_pattern(self): + class ConstantVisitor(CloningVisitor): + def __init__(self): + self.constants = [] + self.fields = [] + self.operations = [] - def visit_field(self, node): - self.fields.append(node.chain) - return super().visit_field(node) + def visit_constant(self, node): + self.constants.append(node.value) + return super().visit_constant(node) - def visit_binary_operation(self, node: ast.BinaryOperation): - self.operations.append(node.op) - return super().visit_binary_operation(node) + def visit_field(self, node): + self.fields.append(node.chain) + return super().visit_field(node) + def visit_binary_operation(self, node: ast.BinaryOperation): + self.operations.append(node.op) + return super().visit_binary_operation(node) -class TestVisitor(BaseTest): - def test_visitor_pattern(self): visitor = ConstantVisitor() visitor.visit(ast.Constant(value="asd")) self.assertEqual(visitor.constants, ["asd"]) @@ -94,3 +93,22 @@ def test_everything_visitor(self): ] ) self.assertEqual(node, CloningVisitor().visit(node)) + + def test_unknown_visitor(self): + class UnknownVisitor(Visitor): + def visit_unknown(self, node): + return "!!" + + def visit_binary_operation(self, node: ast.BinaryOperation): + return self.visit(node.left) + node.op + self.visit(node.right) + + self.assertEqual(UnknownVisitor().visit(parse_expr("1 + 3 / 'asd2'")), "!!+!!/!!") + + def test_unknown_error_visitor(self): + class UnknownNotDefinedVisitor(Visitor): + def visit_binary_operation(self, node: ast.BinaryOperation): + return self.visit(node.left) + node.op + self.visit(node.right) + + with self.assertRaises(ValueError) as e: + UnknownNotDefinedVisitor().visit(parse_expr("1 + 3 / 'asd2'")) + self.assertEqual(str(e.exception), "Visitor has no method visit_constant") From c48024cca6239e86c5a3cbe6e66d86cd834ca22f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 10 Feb 2023 22:56:19 +0100 Subject: [PATCH 020/142] basic printer via a visitor --- posthog/hogql/hogql.py | 2 +- posthog/hogql/printer.py | 195 +++++++++++++++++++++------------------ posthog/hogql/query.py | 6 +- 3 files changed, 111 insertions(+), 92 deletions(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 8e216b1654b46..273cf51ac7864 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -19,4 +19,4 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", raise ValueError(f"SyntaxError: {err.msg}") except NotImplementedError as err: raise ValueError(f"NotImplementedError: {err}") - return print_ast(node, [], context, dialect) + return print_ast(node, context, dialect) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index c61d1f82a8213..553456248b9c9 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -13,6 +13,7 @@ from posthog.hogql.context import HogQLContext, HogQLFieldAccess from posthog.hogql.parser import parse_expr from posthog.hogql.print_string import print_hogql_identifier +from posthog.hogql.visitor import Visitor def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: @@ -30,17 +31,27 @@ def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: return where -def print_ast( - node: ast.AST, stack: List[ast.AST], context: HogQLContext, dialect: Literal["hogql", "clickhouse"] -) -> str: - """Translate a parsed HogQL expression in the shape of a Python AST into a Clickhouse expression.""" - stack.append(node) +def print_ast(node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"]) -> str: + return Printer(context=context, dialect=dialect).visit(node) - if isinstance(node, ast.SelectQuery): - if dialect == "clickhouse" and not context.select_team_id: + +class Printer(Visitor): + def __init__(self, context: HogQLContext, dialect: Literal["hogql", "clickhouse"]): + self.context = context + self.dialect = dialect + self.stack: List[ast.AST] = [] + + def visit(self, node: ast.AST): + self.stack.append(node) + response = super().visit(node) + self.stack.pop() + return response + + def visit_select_query(self, node: ast.SelectQuery): + if self.dialect == "clickhouse" and not self.context.select_team_id: raise ValueError("Full SELECT queries are disabled if select_team_id is not set") - columns = [print_ast(column, stack, context, dialect) for column in node.select] if node.select else ["1"] + columns = [self.visit(column) for column in node.select] if node.select else ["1"] from_table = None if node.select_from: @@ -52,7 +63,7 @@ def print_ast( if node.symbol.print_name: from_table = f"{from_table} AS {node.symbol.print_name}" elif isinstance(node.symbol, ast.SelectQuerySymbol): - from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + from_table = f"({self.visit(node.select_from.table)})" if node.symbol.print_name: from_table = f"{from_table} AS {node.symbol.print_name}" else: @@ -63,7 +74,7 @@ def print_ast( raise ValueError('Only selecting from the "events" table is supported') from_table = "events" elif isinstance(node.select_from.table, ast.SelectQuery): - from_table = f"({print_ast(node.select_from.table, stack, context, dialect)})" + from_table = f"({self.visit(node.select_from.table)})" else: raise ValueError("Only selecting from a table or a subquery is supported") @@ -71,23 +82,23 @@ def print_ast( # Guard with team_id if selecting from a table and printing ClickHouse SQL # We do this in the printer, and not in a separate step, to be really sure this gets added. # This will be improved when we add proper table and column alias support. For now, let's just be safe. - if dialect == "clickhouse" and from_table is not None: - where = guard_where_team_id(where, context) - where = print_ast(where, stack, context, dialect) if where else None + if self.dialect == "clickhouse" and from_table is not None: + where = guard_where_team_id(where, self.context) + where = self.visit(where) if where else None - having = print_ast(node.having, stack, context, dialect) if node.having else None - prewhere = print_ast(node.prewhere, stack, context, dialect) if node.prewhere else None - group_by = [print_ast(column, stack, context, dialect) for column in node.group_by] if node.group_by else None - order_by = [print_ast(column, stack, context, dialect) for column in node.order_by] if node.order_by else None + having = self.visit(node.having) if node.having else None + prewhere = self.visit(node.prewhere) if node.prewhere else None + group_by = [self.visit(column) for column in node.group_by] if node.group_by else None + order_by = [self.visit(column) for column in node.order_by] if node.order_by else None limit = node.limit - if context.limit_top_select: + if self.context.limit_top_select: if limit is not None: if isinstance(limit, ast.Constant) and isinstance(limit.value, int): limit.value = min(limit.value, MAX_SELECT_RETURNED_ROWS) else: limit = ast.Call(name="min2", args=[ast.Constant(value=MAX_SELECT_RETURNED_ROWS), limit]) - elif len(stack) == 1: + elif len(self.stack) == 1: limit = ast.Constant(value=MAX_SELECT_RETURNED_ROWS) clauses = [ @@ -100,116 +111,125 @@ def print_ast( f"ORDER BY {', '.join(order_by)}" if order_by and len(order_by) > 0 else None, ] if limit is not None: - clauses.append(f"LIMIT {print_ast(limit, stack, context, dialect)}") + clauses.append(f"LIMIT {self.visit(limit)}") if node.offset is not None: - clauses.append(f"OFFSET {print_ast(node.offset, stack, context, dialect)}") + clauses.append(f"OFFSET {self.visit(node.offset)}") if node.limit_by is not None: - clauses.append(f"BY {', '.join([print_ast(expr, stack, context, dialect) for expr in node.limit_by])}") + clauses.append(f"BY {', '.join([self.visit(expr) for expr in node.limit_by])}") if node.limit_with_ties: clauses.append("WITH TIES") response = " ".join([clause for clause in clauses if clause]) - if len(stack) > 1: + if len(self.stack) > 1: response = f"({response})" + return response - elif isinstance(node, ast.BinaryOperation): + def visit_binary_operation(self, node: ast.BinaryOperation): if node.op == ast.BinaryOperationType.Add: - response = f"plus({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" + return f"plus({self.visit(node.left)}, {self.visit(node.right)})" elif node.op == ast.BinaryOperationType.Sub: - response = f"minus({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" + return f"minus({self.visit(node.left)}, {self.visit(node.right)})" elif node.op == ast.BinaryOperationType.Mult: - response = f"multiply({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" + return f"multiply({self.visit(node.left)}, {self.visit(node.right)})" elif node.op == ast.BinaryOperationType.Div: - response = f"divide({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" + return f"divide({self.visit(node.left)}, {self.visit(node.right)})" elif node.op == ast.BinaryOperationType.Mod: - response = f"modulo({print_ast(node.left, stack, context, dialect)}, {print_ast(node.right, stack, context, dialect)})" + return f"modulo({self.visit(node.left)}, {self.visit(node.right)})" else: raise ValueError(f"Unknown BinaryOperationType {node.op}") - elif isinstance(node, ast.And): - response = f"and({', '.join([print_ast(operand, stack, context, dialect) for operand in node.exprs])})" - elif isinstance(node, ast.Or): - response = f"or({', '.join([print_ast(operand, stack, context, dialect) for operand in node.exprs])})" - elif isinstance(node, ast.Not): - response = f"not({print_ast(node.expr, stack, context, dialect)})" - elif isinstance(node, ast.OrderExpr): - response = f"{print_ast(node.expr, stack, context, dialect)} {node.order}" - elif isinstance(node, ast.CompareOperation): - left = print_ast(node.left, stack, context, dialect) - right = print_ast(node.right, stack, context, dialect) + + def visit_and(self, node: ast.And): + return f"and({', '.join([self.visit(operand) for operand in node.exprs])})" + + def visit_or(self, node: ast.Or): + return f"or({', '.join([self.visit(operand) for operand in node.exprs])})" + + def visit_not(self, node: ast.Not): + return f"not({self.visit(node.expr)})" + + def visit_order_expr(self, node: ast.OrderExpr): + return f"{self.visit(node.expr)} {node.order}" + + def visit_compare_operation(self, node: ast.CompareOperation): + left = self.visit(node.left) + right = self.visit(node.right) if node.op == ast.CompareOperationType.Eq: if isinstance(node.right, ast.Constant) and node.right.value is None: - response = f"isNull({left})" + return f"isNull({left})" else: - response = f"equals({left}, {right})" + return f"equals({left}, {right})" elif node.op == ast.CompareOperationType.NotEq: if isinstance(node.right, ast.Constant) and node.right.value is None: - response = f"isNotNull({left})" + return f"isNotNull({left})" else: - response = f"notEquals({left}, {right})" + return f"notEquals({left}, {right})" elif node.op == ast.CompareOperationType.Gt: - response = f"greater({left}, {right})" + return f"greater({left}, {right})" elif node.op == ast.CompareOperationType.GtE: - response = f"greaterOrEquals({left}, {right})" + return f"greaterOrEquals({left}, {right})" elif node.op == ast.CompareOperationType.Lt: - response = f"less({left}, {right})" + return f"less({left}, {right})" elif node.op == ast.CompareOperationType.LtE: - response = f"lessOrEquals({left}, {right})" + return f"lessOrEquals({left}, {right})" elif node.op == ast.CompareOperationType.Like: - response = f"like({left}, {right})" + return f"like({left}, {right})" elif node.op == ast.CompareOperationType.ILike: - response = f"ilike({left}, {right})" + return f"ilike({left}, {right})" elif node.op == ast.CompareOperationType.NotLike: - response = f"not(like({left}, {right}))" + return f"not(like({left}, {right}))" elif node.op == ast.CompareOperationType.NotILike: - response = f"not(ilike({left}, {right}))" + return f"not(ilike({left}, {right}))" elif node.op == ast.CompareOperationType.In: - response = f"in({left}, {right})" + return f"in({left}, {right})" elif node.op == ast.CompareOperationType.NotIn: - response = f"not(in({left}, {right}))" + return f"not(in({left}, {right}))" else: raise ValueError(f"Unknown CompareOperationType: {type(node.op).__name__}") - elif isinstance(node, ast.Constant): - key = f"hogql_val_{len(context.values)}" + + def visit_constant(self, node: ast.Constant): + key = f"hogql_val_{len(self.context.values)}" if isinstance(node.value, bool) and node.value is True: - response = "true" + return "true" elif isinstance(node.value, bool) and node.value is False: - response = "false" + return "false" elif isinstance(node.value, int) or isinstance(node.value, float): # :WATCH_OUT: isinstance(True, int) is True (!), so check for numbers lower down the chain - response = str(node.value) + return str(node.value) elif isinstance(node.value, str) or isinstance(node.value, list): - context.values[key] = node.value - response = f"%({key})s" + self.context.values[key] = node.value + return f"%({key})s" elif node.value is None: - response = "null" + return "null" else: raise ValueError( f"Unknown AST Constant node type '{type(node.value).__name__}' for value '{str(node.value)}'" ) - elif isinstance(node, ast.Field): - if dialect == "hogql": + + def visit_field(self, node: ast.Field): + if self.dialect == "hogql": # When printing HogQL, we print the properties out as a chain instead of converting them to Clickhouse SQL - response = ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) + return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) elif node.chain == ["*"]: query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" - response = print_ast(parse_expr(query), stack, context, dialect) + return self.visit(parse_expr(query)) elif node.chain == ["person"]: query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" - response = print_ast(parse_expr(query), stack, context, dialect) + return self.visit(parse_expr(query)) elif node.symbol is not None: if isinstance(node.symbol, ast.FieldSymbol): - response = f"{node.symbol.table.print_name}.{node.symbol.name}" + return f"{node.symbol.table.print_name}.{node.symbol.name}" elif isinstance(node.symbol, ast.TableSymbol): - response = node.symbol.print_name + return node.symbol.print_name else: raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") else: - field_access = parse_field_access(node.chain, context) - context.field_access_logs.append(field_access) - response = field_access.sql - elif isinstance(node, ast.Call): + field_access = parse_field_access(node.chain, self.context) + self.context.field_access_logs.append(field_access) + return field_access.sql + + def visit_call(self, node: ast.Call): if node.name in HOGQL_AGGREGATIONS: - context.found_aggregation = True + self.context.found_aggregation = True required_arg_count = HOGQL_AGGREGATIONS[node.name] if required_arg_count != len(node.args): @@ -218,35 +238,34 @@ def print_ast( ) # check that we're not running inside another aggregate - for stack_node in stack: + for stack_node in self.stack: if stack_node != node and isinstance(stack_node, ast.Call) and stack_node.name in HOGQL_AGGREGATIONS: raise ValueError( f"Aggregation '{node.name}' cannot be nested inside another aggregation '{stack_node.name}'." ) - translated_args = ", ".join([print_ast(arg, stack, context, dialect) for arg in node.args]) - if dialect == "hogql": - response = f"{node.name}({translated_args})" + translated_args = ", ".join([self.visit(arg) for arg in node.args]) + if self.dialect == "hogql": + return f"{node.name}({translated_args})" elif node.name == "count": - response = "count(*)" + return "count(*)" elif node.name == "countDistinct": - response = f"count(distinct {translated_args})" + return f"count(distinct {translated_args})" elif node.name == "countDistinctIf": - response = f"countIf(distinct {translated_args})" + return f"countIf(distinct {translated_args})" else: - response = f"{node.name}({translated_args})" + return f"{node.name}({translated_args})" elif node.name in CLICKHOUSE_FUNCTIONS: - response = f"{CLICKHOUSE_FUNCTIONS[node.name]}({', '.join([print_ast(arg, stack, context, dialect) for arg in node.args])})" + return f"{CLICKHOUSE_FUNCTIONS[node.name]}({', '.join([self.visit(arg) for arg in node.args])})" else: raise ValueError(f"Unsupported function call '{node.name}(...)'") - elif isinstance(node, ast.Placeholder): + + def visit_placeholder(self, node: ast.Placeholder): raise ValueError(f"Found a Placeholder {{{node.field}}} in the tree. Can't generate query!") - else: - raise ValueError(f"Unknown AST node {type(node).__name__}") - stack.pop() - return response + def visit_unknown(self, node: ast.AST): + raise ValueError(f"Unknown AST node {type(node).__name__}") def parse_field_access(chain: List[str], context: HogQLContext) -> HogQLFieldAccess: diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index 362f22dc6cef3..f47e48707a987 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -41,8 +41,8 @@ def execute_hogql_query( hogql_context = HogQLContext(select_team_id=team.pk) resolve_symbols(select_query) - clickhouse = print_ast(select_query, [], hogql_context, "clickhouse") - hogql = print_ast(select_query, [], hogql_context, "hogql") + clickhouse = print_ast(select_query, hogql_context, "clickhouse") + hogql = print_ast(select_query, hogql_context, "hogql") results, types = insight_sync_execute( clickhouse, @@ -51,7 +51,7 @@ def execute_hogql_query( query_type=query_type, workload=workload, ) - print_columns = [print_ast(col, [], HogQLContext(), "hogql") for col in select_query.select] + print_columns = [print_ast(col, HogQLContext(), "hogql") for col in select_query.select] return HogQLQueryResponse( query=query, hogql=hogql, From afe485d56366569611d4bdb9643197d64fea9c9a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 12:34:40 +0100 Subject: [PATCH 021/142] completely redo printing --- posthog/hogql/ast.py | 76 ++++---- posthog/hogql/constants.py | 18 +- posthog/hogql/database.py | 97 ++++++++++ posthog/hogql/hogql.py | 12 +- posthog/hogql/parser.py | 10 +- posthog/hogql/printer.py | 286 ++++++++++++++++------------ posthog/hogql/resolver.py | 103 +++++----- posthog/hogql/test/test_printer.py | 57 ++++-- posthog/hogql/test/test_query.py | 12 ++ posthog/hogql/test/test_resolver.py | 43 +++-- 10 files changed, 463 insertions(+), 251 deletions(-) create mode 100644 posthog/hogql/database.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 014bf81de40f2..3471092833ac9 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Extra -from posthog.hogql.constants import EVENT_FIELDS +from posthog.hogql.database import Table, database # NOTE: when you add new AST fields or nodes, add them to CloningVisitor as well! @@ -27,8 +27,6 @@ def accept(self, visitor): class Symbol(AST): - print_name: Optional[str] - def get_child(self, name: str) -> "Symbol": raise NotImplementedError() @@ -38,20 +36,20 @@ def has_child(self, name: str) -> bool: class ColumnAliasSymbol(Symbol): name: str - symbol: "Symbol" + symbol: Symbol - def get_child(self, name: str) -> "Symbol": + def get_child(self, name: str) -> Symbol: return self.symbol.get_child(name) def has_child(self, name: str) -> bool: return self.symbol.has_child(name) -class TableAliasSymbol(Symbol): +class SelectQueryAliasSymbol(Symbol): name: str - symbol: "Symbol" + symbol: Symbol - def get_child(self, name: str) -> "Symbol": + def get_child(self, name: str) -> Symbol: return self.symbol.get_child(name) def has_child(self, name: str) -> bool: @@ -59,32 +57,39 @@ def has_child(self, name: str) -> bool: class TableSymbol(Symbol): - table_name: Literal["events"] + table: Table def has_child(self, name: str) -> bool: - if self.table_name == "events": - return name in EVENT_FIELDS - else: - raise NotImplementedError(f"Can not resolve table: {self.table_name}") + return name in self.table.__fields__ - def get_child(self, name: str) -> "Symbol": - if self.table_name == "events": - if name in EVENT_FIELDS: - return FieldSymbol(name=name, table=self) - raise NotImplementedError(f"Event field not found: {name}") - else: - raise NotImplementedError(f"Can not resolve table: {self.table_name}") + def get_child(self, name: str) -> Symbol: + if self.has_child(name): + return FieldSymbol(name=name, table=self) + raise NotImplementedError(f"Field not found: {name}") + + +class TableAliasSymbol(Symbol): + name: str + table: TableSymbol + + def get_child(self, name: str) -> Symbol: + return self.table.get_child(name) + + def has_child(self, name: str) -> bool: + return self.table.has_child(name) class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope - aliases: Dict[str, Symbol] + aliases: Dict[str, ColumnAliasSymbol] # all symbols a select query exports columns: Dict[str, Symbol] - # all tables we join in this query on which we look for aliases - tables: Dict[str, Symbol] + # all from and join, tables and subqueries with aliases + tables: Dict[str, Union[TableSymbol, TableAliasSymbol, "SelectQuerySymbol", SelectQueryAliasSymbol]] + # all from and join subqueries without aliases + anonymous_tables: List["SelectQuerySymbol"] - def get_child(self, name: str) -> "Symbol": + def get_child(self, name: str) -> Symbol: if name in self.columns: return self.columns[name] raise NotImplementedError(f"Column not found: {name}") @@ -93,14 +98,22 @@ def has_child(self, name: str) -> bool: return name in self.columns +SelectQuerySymbol.update_forward_refs(SelectQuerySymbol=SelectQuerySymbol) + + +class CallSymbol(Symbol): + name: str + args: List[Symbol] + + class FieldSymbol(Symbol): name: str - table: TableSymbol + table: Union[TableSymbol, TableAliasSymbol] - def get_child(self, name: str) -> "Symbol": - if self.table.table_name == "events": + def get_child(self, name: str) -> Symbol: + if self.table.table == database.events: if self.name == "properties": - raise NotImplementedError(f"Property symbol resolution not implemented yet") + return PropertySymbol(name=name, field=self) else: raise NotImplementedError(f"Can not resolve field {self.name} on table events") else: @@ -120,11 +133,6 @@ class Expr(AST): symbol: Optional[Symbol] -ColumnAliasSymbol.update_forward_refs(Expr=Expr) -TableAliasSymbol.update_forward_refs(Expr=Expr) -SelectQuerySymbol.update_forward_refs(Expr=Expr) - - class Alias(Expr): alias: str expr: Expr @@ -215,6 +223,8 @@ class JoinExpr(Expr): class SelectQuery(Expr): + symbol: Optional[SelectQuerySymbol] = None + select: List[Expr] distinct: Optional[bool] = None select_from: Optional[JoinExpr] = None diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index a7f7a8357c7ff..1cb107817c21c 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -1,18 +1,3 @@ -# fields you can select from in the events query -EVENT_FIELDS = [ - "id", - "uuid", - "event", - "timestamp", - "properties", - "elements_chain", - "created_at", - "distinct_id", - "team_id", -] -# "person.*" fields you can select from in the events query -EVENT_PERSON_FIELDS = ["id", "created_at", "properties"] - # HogQL -> ClickHouse allowed transformations CLICKHOUSE_FUNCTIONS = { # arithmetic @@ -110,6 +95,9 @@ # Keywords passed to ClickHouse without transformation KEYWORDS = ["true", "false", "null"] +# Keywords you can't alias to +RESERVED_KEYWORDS = ["team_id"] + # Allow-listed fields returned when you select "*" from events. Person and group fields will be nested later. SELECT_STAR_FROM_EVENTS_FIELDS = [ "uuid", diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py new file mode 100644 index 0000000000000..031f63502f0ba --- /dev/null +++ b/posthog/hogql/database.py @@ -0,0 +1,97 @@ +from pydantic import BaseModel, Extra + + +class Field(BaseModel): + class Config: + extra = Extra.forbid + + +class IntegerValue(Field): + pass + + +class StringValue(Field): + pass + + +class StringJSONValue(Field): + pass + + +class DateTimeValue(Field): + pass + + +class BooleanValue(Field): + pass + + +class Table(BaseModel): + class Config: + extra = Extra.forbid + + def clickhouse_table(self): + raise NotImplementedError() + + +class PersonsTable(Table): + id: StringValue = StringValue() + created_at: DateTimeValue = DateTimeValue() + team_id: IntegerValue = IntegerValue() + properties: StringJSONValue = StringJSONValue() + is_identified: BooleanValue = BooleanValue() + is_deleted: BooleanValue = BooleanValue() + version: IntegerValue = IntegerValue() + + def clickhouse_table(self): + return "person" + + +class PersonDistinctIdTable(Table): + team_id: IntegerValue = IntegerValue() + distinct_id: StringValue = StringValue() + person_id: StringValue = StringValue() + is_deleted: BooleanValue = BooleanValue() + version: IntegerValue = IntegerValue() + + def clickhouse_table(self): + return "person_distinct_id2" + + +class PersonFieldsOnEvents(Table): + id: StringValue = StringValue() + created_at: DateTimeValue = DateTimeValue() + properties: StringJSONValue = StringJSONValue() + + +class EventsTable(Table): + uuid: StringValue = StringValue() + event: StringValue = StringValue() + timestamp: DateTimeValue = DateTimeValue() + properties: StringJSONValue = StringJSONValue() + elements_chain: StringValue = StringValue() + created_at: DateTimeValue = DateTimeValue() + distinct_id: StringValue = StringValue() + team_id: IntegerValue = IntegerValue() + person: PersonFieldsOnEvents = PersonFieldsOnEvents() + + def clickhouse_table(self): + return "events" + + +# class NumbersTable(Table): +# args: [IntegerValue, IntegerValue] + + +class Database(BaseModel): + class Config: + extra = Extra.forbid + + # All fields below will be tables users can query from + events: EventsTable = EventsTable() + persons: PersonsTable = PersonsTable() + person_distinct_id: PersonDistinctIdTable = PersonDistinctIdTable() + # numbers: NumbersTable = NumbersTable() + + +database = Database() diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 273cf51ac7864..5a10fae468d51 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -1,8 +1,11 @@ from typing import Literal +from posthog.hogql import ast from posthog.hogql.context import HogQLContext +from posthog.hogql.database import database from posthog.hogql.parser import parse_expr, parse_select from posthog.hogql.printer import print_ast +from posthog.hogql.resolver import resolve_symbols def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", "clickhouse"] = "clickhouse") -> str: @@ -13,10 +16,17 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", try: if context.select_team_id: node = parse_select(query, no_placeholders=True) + resolve_symbols(node) + return print_ast(node, context, dialect, stack=[]) else: node = parse_expr(query, no_placeholders=True) + symbol = ast.SelectQuerySymbol( + aliases={}, columns={}, tables={"events": ast.TableSymbol(table=database.events)}, anonymous_tables=[] + ) + resolve_symbols(node, symbol) + return print_ast(node, context, dialect, stack=[ast.SelectQuery(select=[], symbol=symbol)]) + except SyntaxError as err: raise ValueError(f"SyntaxError: {err.msg}") except NotImplementedError as err: raise ValueError(f"NotImplementedError: {err}") - return print_ast(node, context, dialect) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index 2266073968cf0..c9e99c04a70b9 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -4,6 +4,7 @@ from antlr4.error.ErrorListener import ErrorListener from posthog.hogql import ast +from posthog.hogql.constants import KEYWORDS, RESERVED_KEYWORDS from posthog.hogql.grammar.HogQLLexer import HogQLLexer from posthog.hogql.grammar.HogQLParser import HogQLParser from posthog.hogql.parse_string import parse_string, parse_string_literal @@ -321,6 +322,10 @@ def visitColumnExprAlias(self, ctx: HogQLParser.ColumnExprAliasContext): else: raise NotImplementedError(f"Must specify an alias.") expr = self.visit(ctx.columnExpr()) + + if alias in RESERVED_KEYWORDS or alias in KEYWORDS: + raise ValueError(f"Alias '{alias}' is a reserved keyword.") + return ast.Alias(expr=expr, alias=alias) def visitColumnExprExtract(self, ctx: HogQLParser.ColumnExprExtractContext): @@ -541,7 +546,10 @@ def visitTableExprSubquery(self, ctx: HogQLParser.TableExprSubqueryContext): return self.visit(ctx.selectUnionStmt()) def visitTableExprAlias(self, ctx: HogQLParser.TableExprAliasContext): - return ast.JoinExpr(table=self.visit(ctx.tableExpr()), alias=self.visit(ctx.alias() or ctx.identifier())) + alias = self.visit(ctx.alias() or ctx.identifier()) + if alias in RESERVED_KEYWORDS or alias in KEYWORDS: + raise ValueError(f"Alias '{alias}' is a reserved keyword.") + return ast.JoinExpr(table=self.visit(ctx.tableExpr()), alias=alias) def visitTableExprFunction(self, ctx: HogQLParser.TableExprFunctionContext): raise NotImplementedError(f"Unsupported node: TableExprFunction") diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 553456248b9c9..e10a14faa4fde 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1,27 +1,26 @@ -from typing import List, Literal +from typing import List, Literal, Optional, Union +from ee.clickhouse.materialized_columns.columns import get_materialized_columns from posthog.hogql import ast -from posthog.hogql.constants import ( - CLICKHOUSE_FUNCTIONS, - EVENT_FIELDS, - EVENT_PERSON_FIELDS, - HOGQL_AGGREGATIONS, - KEYWORDS, - MAX_SELECT_RETURNED_ROWS, - SELECT_STAR_FROM_EVENTS_FIELDS, -) +from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess -from posthog.hogql.parser import parse_expr from posthog.hogql.print_string import print_hogql_identifier from posthog.hogql.visitor import Visitor -def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: +def guard_where_team_id( + where: Optional[ast.Expr], table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbol], context: HogQLContext +) -> ast.Expr: """Add a mandatory "and(team_id, ...)" filter around the expression.""" if not context.select_team_id: raise ValueError("context.select_team_id not found") - team_clause = parse_expr("team_id = {team_id}", {"team_id": ast.Constant(value=context.select_team_id)}) + team_clause = ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=table_symbol)), + right=ast.Constant(value=context.select_team_id), + ) + if isinstance(where, ast.And): where = ast.And(exprs=[team_clause] + where.exprs) elif where: @@ -31,15 +30,19 @@ def guard_where_team_id(where: ast.Expr, context: HogQLContext) -> ast.Expr: return where -def print_ast(node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"]) -> str: - return Printer(context=context, dialect=dialect).visit(node) +def print_ast( + node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: List[ast.AST] = [] +) -> str: + return Printer(context=context, dialect=dialect, stack=stack).visit(node) class Printer(Visitor): - def __init__(self, context: HogQLContext, dialect: Literal["hogql", "clickhouse"]): + def __init__( + self, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: Optional[List[ast.AST]] = None + ): self.context = context self.dialect = dialect - self.stack: List[ast.AST] = [] + self.stack: List[ast.AST] = stack or [] def visit(self, node: ast.AST): self.stack.append(node) @@ -51,39 +54,52 @@ def visit_select_query(self, node: ast.SelectQuery): if self.dialect == "clickhouse" and not self.context.select_team_id: raise ValueError("Full SELECT queries are disabled if select_team_id is not set") - columns = [self.visit(column) for column in node.select] if node.select else ["1"] + where = node.where - from_table = None - if node.select_from: - if node.symbol: - if isinstance(node.symbol, ast.TableSymbol): - if node.symbol.table_name != "events": - raise ValueError('Only selecting from the "events" table is supported') - from_table = f"events" - if node.symbol.print_name: - from_table = f"{from_table} AS {node.symbol.print_name}" - elif isinstance(node.symbol, ast.SelectQuerySymbol): - from_table = f"({self.visit(node.select_from.table)})" - if node.symbol.print_name: - from_table = f"{from_table} AS {node.symbol.print_name}" - else: + select_from = None + if node.select_from is not None: + if not node.select_from.symbol: + raise ValueError("Printing queries with a FROM clause is not permitted before symbol resolution") + + if node.select_from.join_expr: + raise ValueError("Printing queries with a JOIN clause is not yet permitted") + + if isinstance(node.select_from.symbol, ast.TableAliasSymbol): + table_symbol = node.select_from.symbol.table + if table_symbol is None: + raise ValueError(f"Table alias {node.select_from.symbol.name} does not resolve!") + if not isinstance(table_symbol, ast.TableSymbol): + raise ValueError(f"Table alias {node.select_from.symbol.name} does not resolve to a table!") + select_from = print_hogql_identifier(table_symbol.table.clickhouse_table()) if node.select_from.alias is not None: - raise ValueError("Table aliases not yet supported") - if isinstance(node.select_from.table, ast.Field): - if node.select_from.table.chain != ["events"]: - raise ValueError('Only selecting from the "events" table is supported') - from_table = "events" - elif isinstance(node.select_from.table, ast.SelectQuery): - from_table = f"({self.visit(node.select_from.table)})" - else: - raise ValueError("Only selecting from a table or a subquery is supported") + select_from += f" AS {print_hogql_identifier(node.select_from.alias)}" + + # Guard with team_id if selecting from a table and printing ClickHouse SQL + # We do this in the printer, and not in a separate step, to be really sure this gets added. + # This will be improved when we add proper table and column alias support. For now, let's just be safe. + if self.dialect == "clickhouse": + where = guard_where_team_id(where, node.select_from.symbol, self.context) + + elif isinstance(node.select_from.symbol, ast.TableSymbol): + select_from = print_hogql_identifier(node.select_from.symbol.table.clickhouse_table()) + + # Guard with team_id if selecting from a table and printing ClickHouse SQL + # We do this in the printer, and not in a separate step, to be really sure this gets added. + # This will be improved when we add proper table and column alias support. For now, let's just be safe. + if self.dialect == "clickhouse": + where = guard_where_team_id(where, node.select_from.symbol, self.context) + + elif isinstance(node.select_from.symbol, ast.SelectQuerySymbol): + select_from = self.visit(node.select_from.table) + + elif isinstance(node.select_from.symbol, ast.SelectQueryAliasSymbol) and node.select_from.alias is not None: + select_from = self.visit(node.select_from.table) + select_from += f" AS {print_hogql_identifier(node.select_from.alias)}" + else: + raise ValueError("Only selecting from a table or a subquery is supported") + + columns = [self.visit(column) for column in node.select] if node.select else ["1"] - where = node.where - # Guard with team_id if selecting from a table and printing ClickHouse SQL - # We do this in the printer, and not in a separate step, to be really sure this gets added. - # This will be improved when we add proper table and column alias support. For now, let's just be safe. - if self.dialect == "clickhouse" and from_table is not None: - where = guard_where_team_id(where, self.context) where = self.visit(where) if where else None having = self.visit(node.having) if node.having else None @@ -103,7 +119,7 @@ def visit_select_query(self, node: ast.SelectQuery): clauses = [ f"SELECT {'DISTINCT ' if node.distinct else ''}{', '.join(columns)}", - f"FROM {from_table}" if from_table else None, + f"FROM {select_from}" if select_from else None, "WHERE " + where if where else None, f"GROUP BY {', '.join(group_by)}" if group_by and len(group_by) > 0 else None, "HAVING " + having if having else None, @@ -206,26 +222,34 @@ def visit_constant(self, node: ast.Constant): ) def visit_field(self, node: ast.Field): + original_field = ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) + if node.symbol is None: + raise ValueError(f"Field {original_field} has no symbol") + if self.dialect == "hogql": # When printing HogQL, we print the properties out as a chain instead of converting them to Clickhouse SQL return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) - elif node.chain == ["*"]: - query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" - return self.visit(parse_expr(query)) - elif node.chain == ["person"]: - query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" - return self.visit(parse_expr(query)) + # elif node.chain == ["*"]: + # query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" + # return self.visit(parse_expr(query)) + # elif node.chain == ["person"]: + # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" + # return self.visit(parse_expr(query)) elif node.symbol is not None: - if isinstance(node.symbol, ast.FieldSymbol): - return f"{node.symbol.table.print_name}.{node.symbol.name}" - elif isinstance(node.symbol, ast.TableSymbol): - return node.symbol.print_name - else: - raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") + # find closest select query's symbol for context + select: Optional[ast.SelectQuerySymbol] = None + for stack_node in reversed(self.stack): + if isinstance(stack_node, ast.SelectQuery): + if isinstance(stack_node.symbol, ast.SelectQuerySymbol): + select = stack_node.symbol + break + raise ValueError(f"Closest SelectQuery to field {original_field} has no symbol!") + if select is None: + raise ValueError(f"Can't find SelectQuerySymbol for field: {original_field}") + + return SymbolPrinter(select=select, context=self.context).visit(node.symbol) else: - field_access = parse_field_access(node.chain, self.context) - self.context.field_access_logs.append(field_access) - return field_access.sql + raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") def visit_call(self, node: ast.Call): if node.name in HOGQL_AGGREGATIONS: @@ -249,6 +273,7 @@ def visit_call(self, node: ast.Call): return f"{node.name}({translated_args})" elif node.name == "count": return "count(*)" + # TODO: rework these elif node.name == "countDistinct": return f"count(distinct {translated_args})" elif node.name == "countDistinctIf": @@ -264,68 +289,93 @@ def visit_call(self, node: ast.Call): def visit_placeholder(self, node: ast.Placeholder): raise ValueError(f"Found a Placeholder {{{node.field}}} in the tree. Can't generate query!") + def visit_alias(self, node: ast.Alias): + return f"{self.visit(node.expr)} AS {print_hogql_identifier(node.alias)}" + def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") -def parse_field_access(chain: List[str], context: HogQLContext) -> HogQLFieldAccess: - # Circular import otherwise - from posthog.models.property.util import get_property_string_expr - - """Given a list like ['properties', '$browser'] or ['uuid'], translate to the correct ClickHouse expr.""" - if len(chain) == 2: - if chain[0] == "properties": - key = f"hogql_val_{len(context.values)}" - context.values[key] = chain[1] - escaped_key = f"%({key})s" - expression, _ = get_property_string_expr( - "events", - chain[1], - escaped_key, - "properties", - ) - return HogQLFieldAccess(chain, "event.properties", chain[1], expression) - elif chain[0] == "person": - if chain[1] in EVENT_PERSON_FIELDS: - return HogQLFieldAccess(chain, "person", chain[1], f"person_{chain[1]}") - else: - raise ValueError(f"Unknown person field '{chain[1]}'") - elif len(chain) == 3 and chain[0] == "person" and chain[1] == "properties": - key = f"hogql_val_{len(context.values or {})}" - context.values[key] = chain[2] - escaped_key = f"%({key})s" - - if context.using_person_on_events: - expression, _ = get_property_string_expr( - "events", - chain[2], - escaped_key, - "person_properties", - materialised_table_column="person_properties", +class SymbolPrinter(Visitor): + def __init__(self, select: ast.SelectQuerySymbol, context: HogQLContext): + self.select = select + self.context = context + + def visit_table_symbol(self, symbol: ast.TableSymbol): + return print_hogql_identifier(symbol.table.clickhouse_table()) + + def visit_table_alias_symbol(self, symbol: ast.TableAliasSymbol): + return f"{self.visit(symbol.table)} AS {print_hogql_identifier(symbol.name)}" + + def visit_field_symbol(self, symbol: ast.FieldSymbol): + # do we need a table prefix? + table_prefix = self.visit(symbol.table) + printed_field = print_hogql_identifier(symbol.name) + + field_sql = printed_field + + # Field exists as a column name in this scope. Is it the same field? + if symbol.name in self.select.columns: + column_symbol = self.select.columns[symbol.name] + if column_symbol != symbol: + field_sql = f"{table_prefix}.{printed_field}" + + # Field exists as an alias name in this scope. Is it the same field? + if symbol.name in self.select.aliases: + aliased_symbol = self.select.aliases[symbol.name] + if aliased_symbol != symbol: + field_sql = f"{table_prefix}.{printed_field}" + + if printed_field != "properties": + self.context.field_access_logs.append( + HogQLFieldAccess( + [symbol.name], + "event", + symbol.name, + field_sql, + ) ) + return field_sql + + def visit_property_symbol(self, symbol: ast.PropertySymbol): + key = f"hogql_val_{len(self.context.values)}" + self.context.values[key] = symbol.name + + table = symbol.field.table + if isinstance(table, ast.TableAliasSymbol): + table = table.table + + # TODO: cache this + materialized_columns = get_materialized_columns(table.table.clickhouse_table()) + materialized_column = materialized_columns.get((symbol.name, "properties"), None) + + if materialized_column: + property_sql = print_hogql_identifier(materialized_column) else: - expression, _ = get_property_string_expr( - "person", - chain[2], - escaped_key, - "person_props", - materialised_table_column="properties", + field_sql = self.visit(symbol.field) + property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") + + self.context.field_access_logs.append( + HogQLFieldAccess( + ["properties", symbol.name], + "event.properties", + symbol.name, + property_sql, ) + ) + + return property_sql + + def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): + return print_hogql_identifier(symbol.name) + + def visit_column_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): + return print_hogql_identifier(symbol.name) + + def visit_unknown(self, symbol: ast.AST): + raise ValueError(f"Unknown Symbol {type(symbol).__name__}") - return HogQLFieldAccess(chain, "person.properties", chain[2], expression) - elif len(chain) == 1: - if chain[0] in EVENT_FIELDS: - if chain[0] == "id": - return HogQLFieldAccess(chain, "event", "uuid", "uuid") - elif chain[0] == "properties": - return HogQLFieldAccess(chain, "event", "properties", "properties") - return HogQLFieldAccess(chain, "event", chain[0], chain[0]) - elif chain[0].startswith("person_") and chain[0][7:] in EVENT_PERSON_FIELDS: - return HogQLFieldAccess(chain, "person", chain[0][7:], chain[0]) - elif chain[0].lower() in KEYWORDS: - return HogQLFieldAccess(chain, None, None, chain[0].lower()) - else: - raise ValueError(f"Unknown event field '{chain[0]}'") - raise ValueError(f"Unsupported property access: {chain}") +def trim_quotes_expr(expr: str) -> str: + return f"replaceRegexpAll({expr}, '^\"|\"$', '')" diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index fb0ce78b41aef..8e3cd16f1351a 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -1,13 +1,14 @@ -from typing import Dict, List, Optional +from typing import List, Optional from posthog.hogql import ast +from posthog.hogql.database import database from posthog.hogql.visitor import TraversingVisitor # https://github.com/ClickHouse/ClickHouse/issues/23194 - "Describe how identifiers in SELECT queries are resolved" -def resolve_symbols(node: ast.SelectQuery): - Resolver().visit(node) +def resolve_symbols(node: ast.Expr, scope: Optional[ast.SelectQuerySymbol] = None): + Resolver(scope=scope).visit(node) class ResolverException(ValueError): @@ -17,22 +18,20 @@ class ResolverException(ValueError): class Resolver(TraversingVisitor): """The Resolver visits an AST and assigns Symbols to the nodes.""" - def __init__(self): - self.scopes: List[ast.SelectQuerySymbol] = [] - self.global_tables: Dict[str, ast.Symbol] = {} + def __init__(self, scope: Optional[ast.SelectQuerySymbol] = None): + self.scopes: List[ast.SelectQuerySymbol] = [scope] if scope else [] def visit_select_query(self, node): """Visit each SELECT query or subquery.""" - if node.symbol is not None: return - # Create a new lexical scope each time we enter a SELECT query. - node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}) - # Keep those scopes stacked in a list as we traverse the tree. + node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}, anonymous_tables=[]) + + # Each SELECT query creates a new scope. Store all of them in a list for variable access. self.scopes.append(node.symbol) - # Visit all the FROM and JOIN tables (JoinExpr nodes) + # Visit all the FROM and JOIN clauses (JoinExpr nodes), and register the tables into the scope. if node.select_from: self.visit(node.select_from) @@ -77,41 +76,34 @@ def visit_join_expr(self, node): scope = self.scopes[-1] if isinstance(node.table, ast.Field): - if node.alias is None: - # Make sure there is a way to call the field in the scope. - node.alias = node.table.chain[0] - if node.alias in scope.tables: - raise ResolverException(f'Already have a joined table called "{node.alias}", can\'t redefine.') - - # Only joining the events table is supported - if node.table.chain == ["events"]: - print_name = f"{node.alias[0:1] or 't'}{len(self.global_tables)}" - node.table.symbol = ast.TableSymbol(table_name="events", print_name=print_name) - self.global_tables[print_name] = node.table.symbol - if node.alias == node.table.symbol.table_name: - node.symbol = node.table.symbol - else: - node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol) + table_name = node.table.chain[0] + table_alias = node.alias or table_name + if table_alias in scope.tables: + raise ResolverException(f'Already have a joined table called "{table_alias}", can\'t redefine.') + + if table_name in database.__fields__: + table = database.__fields__[table_name].default + node.symbol = ast.TableSymbol(table=table) + if table_alias != table_name: + node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.symbol) + scope.tables[table_alias] = node.symbol else: - raise ResolverException(f"Cannot resolve table {node.table.chain[0]}") + raise ResolverException(f'Unknown table "{table_name}". Only "events" is supported.') elif isinstance(node.table, ast.SelectQuery): node.table.symbol = self.visit(node.table) - if node.alias is None: - node.symbol = node.table.symbol + if node.alias is not None: + if node.alias in scope.tables: + raise ResolverException(f'Already have a joined table called "{node.alias}", can\'t redefine.') + node.symbol = ast.SelectQueryAliasSymbol(name=node.alias, symbol=node.table.symbol) + scope.tables[node.alias] = node.symbol else: - print_name = self._new_global_table_print_name(node.alias) - node.symbol = ast.TableAliasSymbol(name=node.alias, symbol=node.table.symbol, print_name=print_name) - self.global_tables[print_name] = node.symbol + node.symbol = node.table.symbol + scope.anonymous_tables.append(node.symbol) else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") - scope.tables[node.alias] = node.symbol - - # node.symbol.print_name = self._new_global_table_print_name(node.alias) - # self.global_tables[node.symbol.print_name] = node.symbol - self.visit(node.join_expr) def visit_alias(self, node: ast.Alias): @@ -128,9 +120,19 @@ def visit_alias(self, node: ast.Alias): raise ResolverException("Alias cannot be empty") self.visit(node.expr) - node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=unwrap_column_alias_symbol(node.expr.symbol)) + if not node.expr.symbol: + raise ResolverException(f"Cannot alias an expression without a symbol: {node.alias}") + node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=node.expr.symbol) scope.aliases[node.alias] = node.symbol + def visit_call(self, node: ast.Call): + """Visit function calls.""" + if node.symbol is not None: + return + for arg in node.args: + self.visit(arg) + node.symbol = ast.CallSymbol(name=node.name, args=[arg.symbol for arg in node.args]) + def visit_field(self, node): """Visit a field such as ast.Field(chain=["e", "properties", "$browser"])""" if node.symbol is not None: @@ -142,15 +144,15 @@ def visit_field(self, node): # "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", # # But this is supported: - # "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t", + # "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t JOIN events e ON (e.event = t.event)", # # Thus only look into the current scope, for columns and aliases. scope = self.scopes[-1] symbol: Optional[ast.Symbol] = None name = node.chain[0] + # More than one field and the first one is a table. Found the first match. if len(node.chain) > 1 and name in scope.tables: - # If the field has a chain of at least one (e.g "e", "event"), the first part could refer to a table. symbol = scope.tables[name] elif name in scope.columns: symbol = scope.columns[name] @@ -158,14 +160,17 @@ def visit_field(self, node): symbol = scope.aliases[name] else: # Look through all FROM/JOIN tables, if they export a field by this name. - fields_in_scope = [table.get_child(name) for table in scope.tables.values() if table.has_child(name)] - if len(fields_in_scope) > 1: - raise ResolverException(f'Ambiguous query. Found multiple sources for field "{name}".') - elif len(fields_in_scope) == 1: - symbol = fields_in_scope[0] + tables_with_field = [table for table in scope.tables.values() if table.has_child(name)] + [ + table for table in scope.anonymous_tables if table.has_child(name) + ] + if len(tables_with_field) > 1: + raise ResolverException(f"Ambiguous query. Found multiple sources for field: {name}") + elif len(tables_with_field) == 1: + # accessed a field on a joined table by name + symbol = tables_with_field[0].get_child(name) if not symbol: - raise ResolverException(f'Cannot resolve symbol: "{name}"') + raise ResolverException(f"Unable to resolve field: {name}") # Recursively resolve the rest of the chain until we can point to the deepest node. for child_name in node.chain[1:]: @@ -183,9 +188,6 @@ def visit_constant(self, node): return node.symbol = ast.ConstantSymbol(value=node.value) - def _new_global_table_print_name(self, table_name): - return f"{table_name[0:1] or 't'}{len(self.global_tables)}" - def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: i = 0 @@ -195,3 +197,6 @@ def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: if i > 100: raise ResolverException("ColumnAliasSymbol recursion too deep!") return symbol + + +# select a from (select 1 as a) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index e1e67e8106497..5aea6a45c0747 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -169,10 +169,17 @@ def test_materialized_fields_and_properties(self): self.assertEqual(1 + 2, 3) return materialize("events", "$browser") - self.assertEqual(self._expr("properties['$browser']"), '"mat_$browser"') + self.assertEqual(self._expr("properties['$browser']"), "mat_$browser") + materialize("events", "$browser and string") + self.assertEqual(self._expr("properties['$browser and string']"), "mat_$browser_and_string") + + materialize("events", "$browser%%%#@!@") + self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "mat_$browser_______") + + # TODO: get person properties working materialize("events", "$initial_waffle", table_column="person_properties") - self.assertEqual(self._expr("person.properties['$initial_waffle']"), '"mat_pp_$initial_waffle"') + self.assertEqual(self._expr("person.properties['$initial_waffle']"), "mat_pp_$initial_waffle") def test_methods(self): self.assertEqual(self._expr("count()"), "count(*)") @@ -189,19 +196,19 @@ def test_functions(self): def test_expr_parse_errors(self): self._assert_expr_error("", "Empty query") - self._assert_expr_error("avg(bla)", "Unknown event field 'bla'") + self._assert_expr_error("avg(bla)", "Unable to resolve field: bla") self._assert_expr_error("count(2)", "Aggregation 'count' requires 0 arguments, found 1") self._assert_expr_error("count(2,4)", "Aggregation 'count' requires 0 arguments, found 2") self._assert_expr_error("countIf()", "Aggregation 'countIf' requires 1 argument, found 0") self._assert_expr_error("countIf(2,4)", "Aggregation 'countIf' requires 1 argument, found 2") - self._assert_expr_error("hamburger(bla)", "Unsupported function call 'hamburger(...)'") - self._assert_expr_error("mad(bla)", "Unsupported function call 'mad(...)'") - self._assert_expr_error("yeet.the.cloud", "Unsupported property access: ['yeet', 'the', 'cloud']") - self._assert_expr_error("chipotle", "Unknown event field 'chipotle'") - self._assert_expr_error("person.chipotle", "Unknown person field 'chipotle'") + self._assert_expr_error("hamburger(event)", "Unsupported function call 'hamburger(...)'") + self._assert_expr_error("mad(event)", "Unsupported function call 'mad(...)'") + self._assert_expr_error("yeet.the.cloud", "Unable to resolve field: yeet") + self._assert_expr_error("chipotle", "Unable to resolve field: chipotle") self._assert_expr_error( "avg(avg(properties.bla))", "Aggregation 'avg' cannot be nested inside another aggregation 'avg'." ) + self._assert_expr_error("person.chipotle", "Unknown person field 'chipotle'") def test_expr_syntax_errors(self): self._assert_expr_error("(", "line 1, column 1: no viable alternative at input '('") @@ -211,8 +218,9 @@ def test_expr_syntax_errors(self): self._assert_expr_error( "select query from events", "line 1, column 13: mismatched input 'from' expecting " ) - self._assert_expr_error("this makes little sense", "Unknown AST node Alias") - self._assert_expr_error("event makes little sense", "Unknown AST node Alias") + self._assert_expr_error("this makes little sense", "Unable to resolve field: this") + # TODO: fix + # self._assert_expr_error("event makes little sense", "event AS makes AS little AS sense") self._assert_expr_error("1;2", "line 1, column 1: mismatched input ';' expecting") self._assert_expr_error("b.a(bla)", "SyntaxError: line 1, column 3: mismatched input '(' expecting '.'") @@ -340,9 +348,14 @@ def test_values(self): ) self.assertEqual(context.values, {"hogql_val_0": "E", "hogql_val_1": "lol", "hogql_val_2": "hoo"}) - def test_no_alias_yet(self): - self._assert_expr_error("1 as team_id", "Unknown AST node Alias") - self._assert_expr_error("1 as `-- select team_id`", "Unknown AST node Alias") + def test_alias_keywords(self): + self._assert_expr_error("1 as team_id", "Alias 'team_id' is a reserved keyword.") + self._assert_expr_error("1 as true", "Alias 'true' is a reserved keyword.") + self._assert_select_error("select 1 as team_id from events", "Alias 'team_id' is a reserved keyword.") + self.assertEqual( + self._select("select 1 as `-- select team_id` from events"), + "SELECT 1 AS `-- select team_id` FROM events WHERE equals(team_id, 42) LIMIT 65535", + ) def test_select(self): self.assertEqual(self._select("select 1"), "SELECT 1 LIMIT 65535") @@ -355,14 +368,16 @@ def test_select(self): def test_select_alias(self): # currently not supported! - self._assert_select_error("select 1 as b", "Unknown AST node Alias") - self._assert_select_error("select 1 from events as e", "Table aliases not yet supported") + self.assertEqual(self._select("select 1 as b"), "SELECT 1 AS b LIMIT 65535") + self.assertEqual( + self._select("select 1 from events as e"), "SELECT 1 FROM events AS e WHERE equals(team_id, 42) LIMIT 65535" + ) def test_select_from(self): self.assertEqual( self._select("select 1 from events"), "SELECT 1 FROM events WHERE equals(team_id, 42) LIMIT 65535" ) - self._assert_select_error("select 1 from other", 'Only selecting from the "events" table is supported') + self._assert_select_error("select 1 from other", 'Unknown table "other". Only "events" is supported.') def test_select_where(self): self.assertEqual( @@ -446,3 +461,13 @@ def test_select_distinct(self): self._select("select distinct event from events group by event, timestamp"), "SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp LIMIT 65535", ) + + def test_select_subquery(self): + self.assertEqual( + self._select("SELECT event from (select distinct event from events group by event, timestamp)"), + "SELECT event FROM (SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp) LIMIT 65535", + ) + self.assertEqual( + self._select("SELECT event from (select distinct event from events group by event, timestamp) e"), + "SELECT event FROM (SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp) AS e LIMIT 65535", + ) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 1aec779f121ed..fdf5c263286e1 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -21,4 +21,16 @@ def test_query(self): ) flush_persons_and_events() response = execute_hogql_query("select count(), event from events group by event", self.team) + + response = execute_hogql_query( + "select c.count, event from (select count() as count, event from events) as c group by count, event", + self.team, + ) + + self.assertEqual( + response.clickhouse, + "SELECT count(*), e0.event FROM events e0 WHERE equals(e0.team_id, 1) GROUP BY e0.event LIMIT 1000", + ) + self.assertEqual(response.hogql, "SELECT count(), event FROM events e0 GROUP BY event") self.assertEqual(response.results, [(2, "random event")]) + # self.assertEqual(response.types, []) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index bffe494cb1946..0c522322151fe 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -1,4 +1,5 @@ from posthog.hogql import ast +from posthog.hogql.database import database from posthog.hogql.parser import parse_select from posthog.hogql.resolver import ResolverException, resolve_symbols from posthog.test.base import BaseTest @@ -9,13 +10,14 @@ def test_resolve_events_table(self): expr = parse_select("SELECT event, events.timestamp FROM events WHERE events.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(table_name="events") + events_table_symbol = ast.TableSymbol(table=database.events) event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, tables={"events": events_table_symbol}, + anonymous_tables=[], ) expected = ast.SelectQuery( @@ -47,13 +49,14 @@ def test_resolve_events_table_alias(self): expr = parse_select("SELECT event, e.timestamp FROM events e WHERE e.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(table_name="events") + events_table_symbol = ast.TableSymbol(table=database.events) event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, + anonymous_tables=[], ) expected = ast.SelectQuery( @@ -85,7 +88,7 @@ def test_resolve_events_table_column_alias(self): expr = parse_select("SELECT event as ee, ee, ee as e, e.timestamp FROM events e WHERE e.event = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(table_name="events") + events_table_symbol = ast.TableSymbol(table=database.events) event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) @@ -100,6 +103,7 @@ def test_resolve_events_table_column_alias(self): "timestamp": timestamp_field_symbol, }, tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, + anonymous_tables=[], ) expected = ast.SelectQuery( @@ -139,21 +143,24 @@ def test_resolve_events_table_column_alias(self): def test_resolve_events_table_column_alias_inside_subquery(self): expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") resolve_symbols(expr) - events_table_symbol = ast.TableSymbol(table_name="events") - event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) - timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + outer_events_table_symbol = ast.TableSymbol(table=database.events) + inner_events_table_symbol = ast.TableAliasSymbol(name="e1", symbol=ast.TableSymbol(table=database.events)) + outer_event_field_symbol = ast.FieldSymbol(name="event", table=outer_events_table_symbol) + inner_event_field_symbol = ast.FieldSymbol(name="event", table=inner_events_table_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=outer_events_table_symbol) inner_select_symbol = ast.SelectQuerySymbol( aliases={ - "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), + "b": ast.ColumnAliasSymbol(name="b", symbol=outer_event_field_symbol), "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), }, columns={ - "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), + "b": ast.ColumnAliasSymbol(name="b", symbol=outer_events_table_symbol), "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), }, tables={ - "events": events_table_symbol, + "events": outer_events_table_symbol, }, + anonymous_tables=[], ) expected = ast.SelectQuery( select=[ @@ -161,7 +168,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): chain=["b"], symbol=ast.ColumnAliasSymbol( name="b", - symbol=event_field_symbol, + symbol=outer_event_field_symbol, ), ), ], @@ -170,10 +177,10 @@ def test_resolve_events_table_column_alias_inside_subquery(self): select=[ ast.Alias( alias="b", - expr=ast.Field(chain=["event"], symbol=event_field_symbol), + expr=ast.Field(chain=["event"], symbol=inner_event_field_symbol), symbol=ast.ColumnAliasSymbol( name="b", - symbol=event_field_symbol, + symbol=inner_event_field_symbol, ), ), ast.Alias( @@ -186,9 +193,9 @@ def test_resolve_events_table_column_alias_inside_subquery(self): ), ], select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=events_table_symbol), + table=ast.Field(chain=["events"], symbol=inner_events_table_symbol), alias="events", - symbol=events_table_symbol, + symbol=inner_events_table_symbol, ), symbol=inner_select_symbol, ), @@ -200,7 +207,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): chain=["e", "b"], symbol=ast.ColumnAliasSymbol( name="b", - symbol=event_field_symbol, + symbol=outer_event_field_symbol, ), ), op=ast.CompareOperationType.Eq, @@ -209,7 +216,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): symbol=ast.SelectQuerySymbol( aliases={}, columns={ - "b": ast.ColumnAliasSymbol(name="b", symbol=event_field_symbol), + "b": ast.ColumnAliasSymbol(name="b", symbol=outer_event_field_symbol), }, tables={"e": ast.TableAliasSymbol(name="e", symbol=inner_select_symbol)}, ), @@ -228,7 +235,7 @@ def test_resolve_subquery_no_field_access(self): ) with self.assertRaises(ResolverException) as e: resolve_symbols(expr) - self.assertEqual(str(e.exception), 'Cannot resolve symbol: "e"') + self.assertEqual(str(e.exception), "Unable to resolve field: e") def test_resolve_errors(self): queries = [ @@ -241,7 +248,7 @@ def test_resolve_errors(self): for query in queries: with self.assertRaises(ResolverException) as e: resolve_symbols(parse_select(query)) - self.assertIn("Cannot resolve symbol", str(e.exception)) + self.assertIn("Unable to resolve field: x", str(e.exception)) # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" From 076916e37db2d9c38988dba6c52837a789a98d23 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 15:39:25 +0100 Subject: [PATCH 022/142] some sample queries --- posthog/hogql/test/test_query.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index fdf5c263286e1..459fb93a52e58 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -23,14 +23,17 @@ def test_query(self): response = execute_hogql_query("select count(), event from events group by event", self.team) response = execute_hogql_query( - "select c.count, event from (select count() as count, event from events) as c group by count, event", + "select c.count, event from (select count() as count, event from events group by event) as c group by count, event", self.team, ) self.assertEqual( response.clickhouse, - "SELECT count(*), e0.event FROM events e0 WHERE equals(e0.team_id, 1) GROUP BY e0.event LIMIT 1000", + "SELECT count, event FROM (SELECT count(*) AS count, event FROM events WHERE equals(team_id, 1) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT c.count, event FROM (SELECT count() AS count, event FROM events GROUP BY event) AS c GROUP BY count, event LIMIT 1000", ) - self.assertEqual(response.hogql, "SELECT count(), event FROM events e0 GROUP BY event") self.assertEqual(response.results, [(2, "random event")]) # self.assertEqual(response.types, []) From fd271ddba733aa661670a2c1c8482df7b1647b5f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 16:06:00 +0100 Subject: [PATCH 023/142] query tests --- posthog/hogql/test/test_query.py | 42 +++++++++++++++++++------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 459fb93a52e58..b54595a0de3d3 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -1,39 +1,47 @@ from freezegun import freeze_time from posthog.hogql.query import execute_hogql_query +from posthog.models.utils import UUIDT from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, flush_persons_and_events class TestQuery(ClickhouseTestMixin, APIBaseTest): def test_query(self): with freeze_time("2020-01-10"): - _create_event( - distinct_id="bla", - event="random event", - team=self.team, - properties={"random_prop": "don't include", "some other prop": "with some text"}, - ) - _create_event( - distinct_id="bla", - event="random event", - team=self.team, - properties={"random_prop": "don't include", "some other prop": "with some text"}, - ) + random_uuid = str(UUIDT()) + for index in range(2): + _create_event( + distinct_id="bla", + event="random event", + team=self.team, + properties={"random_prop": "don't include", "random_uuid": random_uuid, "index": index}, + ) flush_persons_and_events() - response = execute_hogql_query("select count(), event from events group by event", self.team) response = execute_hogql_query( - "select c.count, event from (select count() as count, event from events group by event) as c group by count, event", + f"select count(), event from events where properties.random_uuid = '{random_uuid}' group by event", self.team, ) + self.assertEqual( + response.clickhouse, + f"SELECT count(*), event FROM events WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', ''), %(hogql_val_1)s)) GROUP BY event LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT count(), event FROM events WHERE equals(properties.random_uuid, %(hogql_val_2)s) GROUP BY event LIMIT 1000", + ) + self.assertEqual(response.results, [(2, "random event")]) + response = execute_hogql_query( + f"select count, event from (select count() as count, event from events where properties.random_uuid = '{random_uuid}' group by event) as c group by count, event", + self.team, + ) self.assertEqual( response.clickhouse, - "SELECT count, event FROM (SELECT count(*) AS count, event FROM events WHERE equals(team_id, 1) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", + f"SELECT count, event FROM (SELECT count(*) AS count, event FROM events WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', ''), %(hogql_val_1)s)) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", ) self.assertEqual( response.hogql, - "SELECT c.count, event FROM (SELECT count() AS count, event FROM events GROUP BY event) AS c GROUP BY count, event LIMIT 1000", + "SELECT count, event FROM (SELECT count() AS count, event FROM events WHERE equals(properties.random_uuid, %(hogql_val_2)s) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", ) self.assertEqual(response.results, [(2, "random event")]) - # self.assertEqual(response.types, []) From b41055e69c81c36851a334c099da72485b387b17 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 16:38:28 +0100 Subject: [PATCH 024/142] test selecting from persons and distinct id table --- posthog/hogql/ast.py | 13 ++++--- posthog/hogql/test/test_query.py | 59 ++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 3471092833ac9..d79eb6ad208ae 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, Extra -from posthog.hogql.database import Table, database +from posthog.hogql.database import StringJSONValue, Table # NOTE: when you add new AST fields or nodes, add them to CloningVisitor as well! @@ -111,13 +111,16 @@ class FieldSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol] def get_child(self, name: str) -> Symbol: - if self.table.table == database.events: - if self.name == "properties": + db_table = self.table.table + if isinstance(db_table, Table): + if self.name in db_table.__fields__ and isinstance(db_table.__fields__[self.name].default, StringJSONValue): return PropertySymbol(name=name, field=self) else: - raise NotImplementedError(f"Can not resolve field {self.name} on table events") + raise NotImplementedError( + f"Can not access property {name} on field {self.name} because it's not a JSON field" + ) else: - raise NotImplementedError(f"Can not resolve fields on table: {self.name}") + raise NotImplementedError(f"Can not resolve table for field: {self.name}") class ConstantSymbol(Symbol): diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index b54595a0de3d3..06f8a9156a5fe 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -2,21 +2,32 @@ from posthog.hogql.query import execute_hogql_query from posthog.models.utils import UUIDT -from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, flush_persons_and_events +from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, _create_person, flush_persons_and_events class TestQuery(ClickhouseTestMixin, APIBaseTest): + def _create_random_events(self) -> str: + random_uuid = str(UUIDT()) + _create_person( + properties={"email": "tim@posthog.com", "random_uuid": random_uuid}, + team=self.team, + distinct_ids=["bla"], + is_identified=True, + ) + flush_persons_and_events() + for index in range(2): + _create_event( + distinct_id="bla", + event="random event", + team=self.team, + properties={"random_prop": "don't include", "random_uuid": random_uuid, "index": index}, + ) + flush_persons_and_events() + return random_uuid + def test_query(self): with freeze_time("2020-01-10"): - random_uuid = str(UUIDT()) - for index in range(2): - _create_event( - distinct_id="bla", - event="random event", - team=self.team, - properties={"random_prop": "don't include", "random_uuid": random_uuid, "index": index}, - ) - flush_persons_and_events() + random_uuid = self._create_random_events() response = execute_hogql_query( f"select count(), event from events where properties.random_uuid = '{random_uuid}' group by event", @@ -45,3 +56,31 @@ def test_query(self): "SELECT count, event FROM (SELECT count() AS count, event FROM events WHERE equals(properties.random_uuid, %(hogql_val_2)s) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", ) self.assertEqual(response.results, [(2, "random event")]) + + response = execute_hogql_query( + f"select distinct properties.email from persons where properties.random_uuid = '{random_uuid}'", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT DISTINCT replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '') FROM person WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_1)s), '^\"|\"$', ''), %(hogql_val_2)s)) LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT DISTINCT properties.email FROM person WHERE equals(properties.random_uuid, %(hogql_val_3)s) LIMIT 1000", + ) + self.assertEqual(response.results, [("tim@posthog.com",)]) + + response = execute_hogql_query( + f"select distinct person_id, distinct_id from person_distinct_id", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT DISTINCT person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT DISTINCT person_id, distinct_id FROM person_distinct_id2 LIMIT 1000", + ) + self.assertTrue(len(response.results) > 0) From 309a7645572d7f55e3f7d23b80d44f41d9245e54 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 21:27:17 +0100 Subject: [PATCH 025/142] document current join ast --- posthog/hogql/parser.py | 5 +--- posthog/hogql/test/test_parser.py | 46 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index c9e99c04a70b9..955b18427dd5e 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -181,16 +181,13 @@ def visitJoinExprCrossOp(self, ctx: HogQLParser.JoinExprCrossOpContext): def visitJoinOpInner(self, ctx: HogQLParser.JoinOpInnerContext): tokens = [] - if ctx.LEFT(): - tokens.append("INNER") if ctx.ALL(): tokens.append("ALL") - if ctx.ANTI(): - tokens.append("ANTI") if ctx.ANY(): tokens.append("ANY") if ctx.ASOF(): tokens.append("ASOF") + tokens.append("INNER") return " ".join(tokens) def visitJoinOpLeftRight(self, ctx: HogQLParser.JoinOpLeftRightContext): diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index 0313923c96c18..740482c3a7587 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -537,6 +537,52 @@ def test_select_from_join(self): ), ), ) + self.assertEqual( + parse_select( + """ + SELECT event, timestamp, e.distinct_id, p.id, p.properties.email + FROM events e + LEFT JOIN person_distinct_id pdi + ON pdi.distinct_id = e.distinct_id + LEFT JOIN persons p + ON p.id = pdi.person_id + """, + self.team, + ), + ast.SelectQuery( + select=[ + ast.Field(chain=["event"]), + ast.Field(chain=["timestamp"]), + ast.Field(chain=["e", "distinct_id"]), + ast.Field(chain=["p", "id"]), + ast.Field(chain=["p", "properties", "email"]), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"]), + alias="e", + join_type="LEFT JOIN", + join_constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["pdi", "distinct_id"]), + right=ast.Field(chain=["e", "distinct_id"]), + ), + join_expr=ast.JoinExpr( + table=ast.Field(chain=["person_distinct_id"]), + alias="pdi", + join_type="LEFT JOIN", + join_constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["p", "id"]), + right=ast.Field(chain=["pdi", "person_id"]), + ), + join_expr=ast.JoinExpr( + table=ast.Field(chain=["persons"]), + alias="p", + ), + ), + ), + ), + ) def test_select_group_by(self): self.assertEqual( From 592a18cdef1a557200c6b9b52ec07435a2a3e0ca Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 21:28:23 +0100 Subject: [PATCH 026/142] children, joins and aliases --- posthog/hogql/ast.py | 13 +++- posthog/hogql/constants.py | 2 + posthog/hogql/printer.py | 127 ++++++++++++++++--------------- posthog/hogql/resolver.py | 49 +++++------- posthog/hogql/test/test_query.py | 46 +++++++++++ 5 files changed, 145 insertions(+), 92 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d79eb6ad208ae..784c31222ec36 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -72,12 +72,14 @@ class TableAliasSymbol(Symbol): name: str table: TableSymbol - def get_child(self, name: str) -> Symbol: - return self.table.get_child(name) - def has_child(self, name: str) -> bool: return self.table.has_child(name) + def get_child(self, name: str) -> Symbol: + if self.has_child(name): + return FieldSymbol(name=name, table=self) + return self.table.get_child(name) + class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope @@ -111,7 +113,10 @@ class FieldSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol] def get_child(self, name: str) -> Symbol: - db_table = self.table.table + table_symbol = self.table + while isinstance(table_symbol, TableAliasSymbol): + table_symbol = table_symbol.table + db_table = table_symbol.table if isinstance(db_table, Table): if self.name in db_table.__fields__ and isinstance(db_table.__fields__[self.name].default, StringJSONValue): return PropertySymbol(name=name, field=self) diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 1cb107817c21c..0a42c31f7d500 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -91,6 +91,8 @@ "avgIf": 2, "any": 1, "anyIf": 2, + "argMax": 2, + "argMin": 2, } # Keywords passed to ClickHouse without transformation KEYWORDS = ["true", "false", "null"] diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index e10a14faa4fde..959a1fe7553d3 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -5,6 +5,7 @@ from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess from posthog.hogql.print_string import print_hogql_identifier +from posthog.hogql.resolver import ResolverException, lookup_field_by_name from posthog.hogql.visitor import Visitor @@ -44,6 +45,12 @@ def __init__( self.dialect = dialect self.stack: List[ast.AST] = stack or [] + def _last_select(self) -> Optional[ast.SelectQuery]: + for node in reversed(self.stack): + if isinstance(node, ast.SelectQuery): + return node + return None + def visit(self, node: ast.AST): self.stack.append(node) response = super().visit(node) @@ -56,47 +63,11 @@ def visit_select_query(self, node: ast.SelectQuery): where = node.where - select_from = None + select_from = "" if node.select_from is not None: if not node.select_from.symbol: raise ValueError("Printing queries with a FROM clause is not permitted before symbol resolution") - - if node.select_from.join_expr: - raise ValueError("Printing queries with a JOIN clause is not yet permitted") - - if isinstance(node.select_from.symbol, ast.TableAliasSymbol): - table_symbol = node.select_from.symbol.table - if table_symbol is None: - raise ValueError(f"Table alias {node.select_from.symbol.name} does not resolve!") - if not isinstance(table_symbol, ast.TableSymbol): - raise ValueError(f"Table alias {node.select_from.symbol.name} does not resolve to a table!") - select_from = print_hogql_identifier(table_symbol.table.clickhouse_table()) - if node.select_from.alias is not None: - select_from += f" AS {print_hogql_identifier(node.select_from.alias)}" - - # Guard with team_id if selecting from a table and printing ClickHouse SQL - # We do this in the printer, and not in a separate step, to be really sure this gets added. - # This will be improved when we add proper table and column alias support. For now, let's just be safe. - if self.dialect == "clickhouse": - where = guard_where_team_id(where, node.select_from.symbol, self.context) - - elif isinstance(node.select_from.symbol, ast.TableSymbol): - select_from = print_hogql_identifier(node.select_from.symbol.table.clickhouse_table()) - - # Guard with team_id if selecting from a table and printing ClickHouse SQL - # We do this in the printer, and not in a separate step, to be really sure this gets added. - # This will be improved when we add proper table and column alias support. For now, let's just be safe. - if self.dialect == "clickhouse": - where = guard_where_team_id(where, node.select_from.symbol, self.context) - - elif isinstance(node.select_from.symbol, ast.SelectQuerySymbol): - select_from = self.visit(node.select_from.table) - - elif isinstance(node.select_from.symbol, ast.SelectQueryAliasSymbol) and node.select_from.alias is not None: - select_from = self.visit(node.select_from.table) - select_from += f" AS {print_hogql_identifier(node.select_from.alias)}" - else: - raise ValueError("Only selecting from a table or a subquery is supported") + select_from = self.visit(node.select_from) columns = [self.visit(column) for column in node.select] if node.select else ["1"] @@ -140,6 +111,52 @@ def visit_select_query(self, node: ast.SelectQuery): response = f"({response})" return response + def visit_join_expr(self, node: ast.JoinExpr) -> str: + select_from = [] + if isinstance(node.symbol, ast.TableAliasSymbol): + table_symbol = node.symbol.table + if table_symbol is None: + raise ValueError(f"Table alias {node.symbol.name} does not resolve!") + if not isinstance(table_symbol, ast.TableSymbol): + raise ValueError(f"Table alias {node.symbol.name} does not resolve to a table!") + select_from.append(print_hogql_identifier(table_symbol.table.clickhouse_table())) + if node.alias is not None: + select_from.append(f"AS {print_hogql_identifier(node.alias)}") + + # Guard with team_id if selecting from a table and printing ClickHouse SQL + # We do this in the printer, and not in a separate step, to be really sure this gets added. + # This will be improved when we add proper table and column alias support. For now, let's just be safe. + if self.dialect == "clickhouse": + select = self._last_select() + select.where = guard_where_team_id(select.where, node.symbol, self.context) + + elif isinstance(node.symbol, ast.TableSymbol): + select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) + + # Guard with team_id if selecting from a table and printing ClickHouse SQL + # We do this in the printer, and not in a separate step, to be really sure this gets added. + # This will be improved when we add proper table and column alias support. For now, let's just be safe. + if self.dialect == "clickhouse": + select = self._last_select() + select.where = guard_where_team_id(select.where, node.symbol, self.context) + + elif isinstance(node.symbol, ast.SelectQuerySymbol): + select_from.append(self.visit(node.table)) + + elif isinstance(node.symbol, ast.SelectQueryAliasSymbol) and node.alias is not None: + select_from.append(self.visit(node.table)) + select_from.append(f"AS {print_hogql_identifier(node.alias)}") + else: + raise ValueError("Only selecting from a table or a subquery is supported") + + if node.join_type is not None and node.join_expr is not None: + select_from.append(f"{node.join_type} {self.visit(node.join_expr)}") + + if node.join_constraint is not None: + select_from.append(f"ON {self.visit(node.join_constraint)}") + + return " ".join(select_from) + def visit_binary_operation(self, node: ast.BinaryOperation): if node.op == ast.BinaryOperationType.Add: return f"plus({self.visit(node.left)}, {self.visit(node.right)})" @@ -236,17 +253,10 @@ def visit_field(self, node: ast.Field): # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" # return self.visit(parse_expr(query)) elif node.symbol is not None: - # find closest select query's symbol for context - select: Optional[ast.SelectQuerySymbol] = None - for stack_node in reversed(self.stack): - if isinstance(stack_node, ast.SelectQuery): - if isinstance(stack_node.symbol, ast.SelectQuerySymbol): - select = stack_node.symbol - break - raise ValueError(f"Closest SelectQuery to field {original_field} has no symbol!") + select_query = self._last_select() + select: Optional[ast.SelectQuerySymbol] = select_query.symbol if select_query else None if select is None: raise ValueError(f"Can't find SelectQuerySymbol for field: {original_field}") - return SymbolPrinter(select=select, context=self.context).visit(node.symbol) else: raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") @@ -305,28 +315,25 @@ def visit_table_symbol(self, symbol: ast.TableSymbol): return print_hogql_identifier(symbol.table.clickhouse_table()) def visit_table_alias_symbol(self, symbol: ast.TableAliasSymbol): - return f"{self.visit(symbol.table)} AS {print_hogql_identifier(symbol.name)}" + return print_hogql_identifier(symbol.name) def visit_field_symbol(self, symbol: ast.FieldSymbol): # do we need a table prefix? table_prefix = self.visit(symbol.table) printed_field = print_hogql_identifier(symbol.name) - field_sql = printed_field - - # Field exists as a column name in this scope. Is it the same field? - if symbol.name in self.select.columns: - column_symbol = self.select.columns[symbol.name] - if column_symbol != symbol: - field_sql = f"{table_prefix}.{printed_field}" + try: + symbol_with_name_in_scope = lookup_field_by_name(self.select, symbol.name) + except ResolverException: + symbol_with_name_in_scope = None - # Field exists as an alias name in this scope. Is it the same field? - if symbol.name in self.select.aliases: - aliased_symbol = self.select.aliases[symbol.name] - if aliased_symbol != symbol: - field_sql = f"{table_prefix}.{printed_field}" + if symbol_with_name_in_scope == symbol: + field_sql = printed_field + else: + field_sql = f"{table_prefix}.{printed_field}" if printed_field != "properties": + # TODO: refactor this property access logging self.context.field_access_logs.append( HogQLFieldAccess( [symbol.name], diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 8e3cd16f1351a..27a9f5fc72011 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -88,7 +88,7 @@ def visit_join_expr(self, node): node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.symbol) scope.tables[table_alias] = node.symbol else: - raise ResolverException(f'Unknown table "{table_name}". Only "events" is supported.') + raise ResolverException(f'Unknown table "{table_name}".') elif isinstance(node.table, ast.SelectQuery): node.table.symbol = self.visit(node.table) @@ -105,6 +105,7 @@ def visit_join_expr(self, node): raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") self.visit(node.join_expr) + self.visit(node.join_constraint) def visit_alias(self, node: ast.Alias): """Visit column aliases. SELECT 1, (select 3 as y) as x.""" @@ -151,24 +152,11 @@ def visit_field(self, node): symbol: Optional[ast.Symbol] = None name = node.chain[0] - # More than one field and the first one is a table. Found the first match. + # Only look for matching tables if field contains at least two parts. if len(node.chain) > 1 and name in scope.tables: symbol = scope.tables[name] - elif name in scope.columns: - symbol = scope.columns[name] - elif name in scope.aliases: - symbol = scope.aliases[name] - else: - # Look through all FROM/JOIN tables, if they export a field by this name. - tables_with_field = [table for table in scope.tables.values() if table.has_child(name)] + [ - table for table in scope.anonymous_tables if table.has_child(name) - ] - if len(tables_with_field) > 1: - raise ResolverException(f"Ambiguous query. Found multiple sources for field: {name}") - elif len(tables_with_field) == 1: - # accessed a field on a joined table by name - symbol = tables_with_field[0].get_child(name) - + if not symbol: + symbol = lookup_field_by_name(scope, name) if not symbol: raise ResolverException(f"Unable to resolve field: {name}") @@ -189,14 +177,19 @@ def visit_constant(self, node): node.symbol = ast.ConstantSymbol(value=node.value) -def unwrap_column_alias_symbol(symbol: ast.Symbol) -> ast.Symbol: - i = 0 - while isinstance(symbol, ast.ColumnAliasSymbol): - symbol = symbol.symbol - i += 1 - if i > 100: - raise ResolverException("ColumnAliasSymbol recursion too deep!") - return symbol - - -# select a from (select 1 as a) +def lookup_field_by_name(scope: ast.SelectQuerySymbol, name: str) -> Optional[ast.Symbol]: + if name in scope.columns: + return scope.columns[name] + elif name in scope.aliases: + return scope.aliases[name] + else: + named_tables = [table for table in scope.tables.values() if table.has_child(name)] + anonymous_tables = [table for table in scope.anonymous_tables if table.has_child(name)] + tables = named_tables + anonymous_tables + + if len(tables) > 1: + raise ResolverException(f"Ambiguous query. Found multiple sources for field: {name}") + elif len(tables) == 1: + # accessed a field on a joined table by name + return tables[0].get_child(name) + return None diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 06f8a9156a5fe..27ad2b99f6e03 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -84,3 +84,49 @@ def test_query(self): "SELECT DISTINCT person_id, distinct_id FROM person_distinct_id2 LIMIT 1000", ) self.assertTrue(len(response.results) > 0) + + def test_query_joins_simple(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + """ + SELECT event, timestamp, e.distinct_id, p.id, p.properties.email + FROM events e + LEFT JOIN person_distinct_id pdi + ON pdi.distinct_id = e.distinct_id + LEFT JOIN persons p + ON p.id = pdi.person_id + """, + self.team, + ) + self.assertEqual( + response.clickhouse, + f"", + ) + + def test_query_joins(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + """select event, timestamp, pdi.person_id from events e INNER JOIN ( + SELECT distinct_id, + argMax(person_id, version) as person_id + FROM person_distinct_id + WHERE team_id = 1 + GROUP BY distinct_id + HAVING argMax(is_deleted, version) = 0 + ) AS pdi + ON e.distinct_id = pdi.distinct_id""", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT DISTINCT person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT DISTINCT person_id, distinct_id FROM person_distinct_id LIMIT 1000", + ) + self.assertTrue(len(response.results) > 0) From 72da1d92c2cd664d165a47874e92abf76b2cc391 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 22:04:29 +0100 Subject: [PATCH 027/142] turn joins around --- posthog/hogql/ast.py | 8 +-- posthog/hogql/parser.py | 16 +++--- posthog/hogql/printer.py | 27 ++++++---- posthog/hogql/resolver.py | 4 +- posthog/hogql/test/test_parser.py | 81 +++++++++++++++++------------- posthog/hogql/test/test_printer.py | 2 +- posthog/hogql/test/test_visitor.py | 14 +++--- posthog/hogql/visitor.py | 8 +-- 8 files changed, 87 insertions(+), 73 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 784c31222ec36..d50181ba1196c 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -222,12 +222,12 @@ class Call(Expr): class JoinExpr(Expr): + join_type: Optional[str] = None table: Optional[Union["SelectQuery", Field]] = None - table_final: Optional[bool] = None alias: Optional[str] = None - join_type: Optional[str] = None - join_constraint: Optional[Expr] = None - join_expr: Optional["JoinExpr"] = None + table_final: Optional[bool] = None + constraint: Optional[Expr] = None + next_join: Optional["JoinExpr"] = None class SelectQuery(Expr): diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index 955b18427dd5e..f0893f9aa6eb5 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -148,18 +148,16 @@ def visitJoinExprOp(self, ctx: HogQLParser.JoinExprOpContext): join2: ast.JoinExpr = self.visit(ctx.joinExpr(1)) if ctx.joinOp(): - join_type = f"{self.visit(ctx.joinOp())} JOIN" + join2.join_type = f"{self.visit(ctx.joinOp())} JOIN" else: - join_type = "JOIN" - join_constraint = self.visit(ctx.joinConstraintClause()) + join2.join_type = "JOIN" + join2.constraint = self.visit(ctx.joinConstraintClause()) - join_without_next_expr = join1 - while join_without_next_expr.join_expr: - join_without_next_expr = join_without_next_expr.join_expr + last_join = join1 + while last_join.next_join is not None: + last_join = last_join.next_join + last_join.next_join = join2 - join_without_next_expr.join_expr = join2 - join_without_next_expr.join_constraint = join_constraint - join_without_next_expr.join_type = join_type return join1 def visitJoinExprTable(self, ctx: HogQLParser.JoinExprTableContext): diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 959a1fe7553d3..f327a082a4e23 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -61,18 +61,15 @@ def visit_select_query(self, node: ast.SelectQuery): if self.dialect == "clickhouse" and not self.context.select_team_id: raise ValueError("Full SELECT queries are disabled if select_team_id is not set") - where = node.where - select_from = "" - if node.select_from is not None: + if isinstance(node.select_from, ast.JoinExpr): if not node.select_from.symbol: raise ValueError("Printing queries with a FROM clause is not permitted before symbol resolution") select_from = self.visit(node.select_from) columns = [self.visit(column) for column in node.select] if node.select else ["1"] - where = self.visit(where) if where else None - + where = self.visit(node.where) if node.where else None having = self.visit(node.having) if node.having else None prewhere = self.visit(node.prewhere) if node.prewhere else None group_by = [self.visit(column) for column in node.group_by] if node.group_by else None @@ -113,6 +110,9 @@ def visit_select_query(self, node: ast.SelectQuery): def visit_join_expr(self, node: ast.JoinExpr) -> str: select_from = [] + if node.join_type is not None: + select_from.append(node.join_type) + if isinstance(node.symbol, ast.TableAliasSymbol): table_symbol = node.symbol.table if table_symbol is None: @@ -128,7 +128,8 @@ def visit_join_expr(self, node: ast.JoinExpr) -> str: # This will be improved when we add proper table and column alias support. For now, let's just be safe. if self.dialect == "clickhouse": select = self._last_select() - select.where = guard_where_team_id(select.where, node.symbol, self.context) + if select is not None: + select.where = guard_where_team_id(select.where, node.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) @@ -138,7 +139,8 @@ def visit_join_expr(self, node: ast.JoinExpr) -> str: # This will be improved when we add proper table and column alias support. For now, let's just be safe. if self.dialect == "clickhouse": select = self._last_select() - select.where = guard_where_team_id(select.where, node.symbol, self.context) + if select is not None: + select.where = guard_where_team_id(select.where, node.symbol, self.context) elif isinstance(node.symbol, ast.SelectQuerySymbol): select_from.append(self.visit(node.table)) @@ -149,11 +151,14 @@ def visit_join_expr(self, node: ast.JoinExpr) -> str: else: raise ValueError("Only selecting from a table or a subquery is supported") - if node.join_type is not None and node.join_expr is not None: - select_from.append(f"{node.join_type} {self.visit(node.join_expr)}") + if node.table_final: + select_from.append("FINAL") + + if node.constraint is not None: + select_from.append(f"ON {self.visit(node.constraint)}") - if node.join_constraint is not None: - select_from.append(f"ON {self.visit(node.join_constraint)}") + if node.next_join is not None: + select_from.append(self.visit(node.next_join)) return " ".join(select_from) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 27a9f5fc72011..f2be08fdbadf1 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -104,8 +104,8 @@ def visit_join_expr(self, node): else: raise ResolverException(f"JoinExpr with table of type {type(node.table).__name__} not supported") - self.visit(node.join_expr) - self.visit(node.join_constraint) + self.visit(node.constraint) + self.visit(node.next_join) def visit_alias(self, node: ast.Alias): """Visit column aliases. SELECT 1, (select 3 as y) as x.""" diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index 740482c3a7587..8c51fc282e1f7 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -502,9 +502,11 @@ def test_select_from_join(self): select=[ast.Constant(value=1)], select_from=ast.JoinExpr( table=ast.Field(chain=["events"]), - join_type="JOIN", - join_constraint=ast.Constant(value=1), - join_expr=ast.JoinExpr(table=ast.Field(chain=["events2"])), + next_join=ast.JoinExpr( + join_type="JOIN", + table=ast.Field(chain=["events2"]), + constraint=ast.Constant(value=1), + ), ), ), ) @@ -514,9 +516,11 @@ def test_select_from_join(self): select=[ast.Field(chain=["*"])], select_from=ast.JoinExpr( table=ast.Field(chain=["events"]), - join_type="LEFT OUTER JOIN", - join_constraint=ast.Constant(value=1), - join_expr=ast.JoinExpr(table=ast.Field(chain=["events2"])), + next_join=ast.JoinExpr( + join_type="LEFT OUTER JOIN", + table=ast.Field(chain=["events2"]), + constraint=ast.Constant(value=1), + ), ), ), ) @@ -526,29 +530,34 @@ def test_select_from_join(self): select=[ast.Constant(value=1)], select_from=ast.JoinExpr( table=ast.Field(chain=["events"]), - join_type="LEFT OUTER JOIN", - join_constraint=ast.Constant(value=1), - join_expr=ast.JoinExpr( + next_join=ast.JoinExpr( + join_type="LEFT OUTER JOIN", table=ast.Field(chain=["events2"]), - join_type="RIGHT ANY JOIN", - join_constraint=ast.Constant(value=2), - join_expr=ast.JoinExpr(table=ast.Field(chain=["events3"])), + constraint=ast.Constant(value=1), + next_join=ast.JoinExpr( + join_type="RIGHT ANY JOIN", + table=ast.Field(chain=["events3"]), + constraint=ast.Constant(value=2), + ), ), ), ), ) - self.assertEqual( - parse_select( - """ - SELECT event, timestamp, e.distinct_id, p.id, p.properties.email - FROM events e - LEFT JOIN person_distinct_id pdi - ON pdi.distinct_id = e.distinct_id - LEFT JOIN persons p - ON p.id = pdi.person_id - """, - self.team, - ), + + def test_select_from_join_multiple(self): + node = parse_select( + """ + SELECT event, timestamp, e.distinct_id, p.id, p.properties.email + FROM events e + LEFT JOIN person_distinct_id pdi + ON pdi.distinct_id = e.distinct_id + LEFT JOIN persons p + ON p.id = pdi.person_id + """, + self.team, + ) + self.assertEqual( + node, ast.SelectQuery( select=[ ast.Field(chain=["event"]), @@ -560,24 +569,24 @@ def test_select_from_join(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"]), alias="e", - join_type="LEFT JOIN", - join_constraint=ast.CompareOperation( - op=ast.CompareOperationType.Eq, - left=ast.Field(chain=["pdi", "distinct_id"]), - right=ast.Field(chain=["e", "distinct_id"]), - ), - join_expr=ast.JoinExpr( + next_join=ast.JoinExpr( + join_type="LEFT JOIN", table=ast.Field(chain=["person_distinct_id"]), alias="pdi", - join_type="LEFT JOIN", - join_constraint=ast.CompareOperation( + constraint=ast.CompareOperation( op=ast.CompareOperationType.Eq, - left=ast.Field(chain=["p", "id"]), - right=ast.Field(chain=["pdi", "person_id"]), + left=ast.Field(chain=["pdi", "distinct_id"]), + right=ast.Field(chain=["e", "distinct_id"]), ), - join_expr=ast.JoinExpr( + next_join=ast.JoinExpr( + join_type="LEFT JOIN", table=ast.Field(chain=["persons"]), alias="p", + constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["p", "id"]), + right=ast.Field(chain=["pdi", "person_id"]), + ), ), ), ), diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 5aea6a45c0747..fe082d123cc22 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -377,7 +377,7 @@ def test_select_from(self): self.assertEqual( self._select("select 1 from events"), "SELECT 1 FROM events WHERE equals(team_id, 42) LIMIT 65535" ) - self._assert_select_error("select 1 from other", 'Unknown table "other". Only "events" is supported.') + self._assert_select_error("select 1 from other", 'Unknown table "other".') def test_select_where(self): self.assertEqual( diff --git a/posthog/hogql/test/test_visitor.py b/posthog/hogql/test/test_visitor.py index 0a67144f659cc..dee4fe474bca4 100644 --- a/posthog/hogql/test/test_visitor.py +++ b/posthog/hogql/test/test_visitor.py @@ -71,13 +71,15 @@ def test_everything_visitor(self): table=ast.Field(chain=["b"]), table_final=True, alias="c", - join_type="INNER", - join_constraint=ast.CompareOperation( - op=ast.CompareOperationType.Eq, - left=ast.Field(chain=["d"]), - right=ast.Field(chain=["e"]), + next_join=ast.JoinExpr( + join_type="INNER", + table=ast.Field(chain=["f"]), + constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=["d"]), + right=ast.Field(chain=["e"]), + ), ), - join_expr=ast.JoinExpr(table=ast.Field(chain=["f"])), ), where=ast.Constant(value=True), prewhere=ast.Constant(value=True), diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index fce103f70e28f..b5628f5d7287c 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -54,8 +54,8 @@ def visit_call(self, node: ast.Call): def visit_join_expr(self, node: ast.JoinExpr): self.visit(node.table) - self.visit(node.join_expr) - self.visit(node.join_constraint) + self.visit(node.constraint) + self.visit(node.next_join) def visit_select_query(self, node: ast.SelectQuery): self.visit(node.select_from) @@ -133,11 +133,11 @@ def visit_call(self, node: ast.Call): def visit_join_expr(self, node: ast.JoinExpr): return ast.JoinExpr( table=self.visit(node.table), - join_expr=self.visit(node.join_expr), + next_join=self.visit(node.next_join), table_final=node.table_final, alias=node.alias, join_type=node.join_type, - join_constraint=self.visit(node.join_constraint), + constraint=self.visit(node.constraint), ) def visit_select_query(self, node: ast.SelectQuery): From 54e55398d948e892ce631d8b328631df80e1ec7a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 23:03:11 +0100 Subject: [PATCH 028/142] get some actual joins working --- posthog/hogql/printer.py | 63 ++++++++++++++++++-------------- posthog/hogql/resolver.py | 4 +- posthog/hogql/test/test_query.py | 11 +++++- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index f327a082a4e23..3bdffc29d0804 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -61,15 +61,30 @@ def visit_select_query(self, node: ast.SelectQuery): if self.dialect == "clickhouse" and not self.context.select_team_id: raise ValueError("Full SELECT queries are disabled if select_team_id is not set") - select_from = "" - if isinstance(node.select_from, ast.JoinExpr): - if not node.select_from.symbol: + # we will add extra clauses onto this + where = node.where + + select_from = [] + next_join = node.select_from + while isinstance(next_join, ast.JoinExpr): + if next_join.symbol is None: raise ValueError("Printing queries with a FROM clause is not permitted before symbol resolution") - select_from = self.visit(node.select_from) - columns = [self.visit(column) for column in node.select] if node.select else ["1"] + (select_sql, extra_where) = self.visit_join_expr(next_join) + select_from.append(select_sql) - where = self.visit(node.where) if node.where else None + if extra_where is not None: + if where is None: + where = extra_where + elif isinstance(where, ast.And): + where = ast.And(exprs=where.exprs + [extra_where]) + else: + where = ast.And(exprs=[where, extra_where]) + + next_join = next_join.next_join + + columns = [self.visit(column) for column in node.select] if node.select else ["1"] + where = self.visit(where) if where else None having = self.visit(node.having) if node.having else None prewhere = self.visit(node.prewhere) if node.prewhere else None group_by = [self.visit(column) for column in node.group_by] if node.group_by else None @@ -87,7 +102,7 @@ def visit_select_query(self, node: ast.SelectQuery): clauses = [ f"SELECT {'DISTINCT ' if node.distinct else ''}{', '.join(columns)}", - f"FROM {select_from}" if select_from else None, + f"FROM {' '.join(select_from)}" if len(select_from) > 0 else None, "WHERE " + where if where else None, f"GROUP BY {', '.join(group_by)}" if group_by and len(group_by) > 0 else None, "HAVING " + having if having else None, @@ -108,7 +123,10 @@ def visit_select_query(self, node: ast.SelectQuery): response = f"({response})" return response - def visit_join_expr(self, node: ast.JoinExpr) -> str: + def visit_join_expr(self, node: ast.JoinExpr) -> (str, Optional[ast.Expr]): + # return constraints we must place on the select query + extra_where = None + select_from = [] if node.join_type is not None: select_from.append(node.join_type) @@ -123,24 +141,14 @@ def visit_join_expr(self, node: ast.JoinExpr) -> str: if node.alias is not None: select_from.append(f"AS {print_hogql_identifier(node.alias)}") - # Guard with team_id if selecting from a table and printing ClickHouse SQL - # We do this in the printer, and not in a separate step, to be really sure this gets added. - # This will be improved when we add proper table and column alias support. For now, let's just be safe. if self.dialect == "clickhouse": - select = self._last_select() - if select is not None: - select.where = guard_where_team_id(select.where, node.symbol, self.context) + extra_where = guard_where_team_id(None, node.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) - # Guard with team_id if selecting from a table and printing ClickHouse SQL - # We do this in the printer, and not in a separate step, to be really sure this gets added. - # This will be improved when we add proper table and column alias support. For now, let's just be safe. if self.dialect == "clickhouse": - select = self._last_select() - if select is not None: - select.where = guard_where_team_id(select.where, node.symbol, self.context) + extra_where = guard_where_team_id(None, node.symbol, self.context) elif isinstance(node.symbol, ast.SelectQuerySymbol): select_from.append(self.visit(node.table)) @@ -157,10 +165,7 @@ def visit_join_expr(self, node: ast.JoinExpr) -> str: if node.constraint is not None: select_from.append(f"ON {self.visit(node.constraint)}") - if node.next_join is not None: - select_from.append(self.visit(node.next_join)) - - return " ".join(select_from) + return (" ".join(select_from), extra_where) def visit_binary_operation(self, node: ast.BinaryOperation): if node.op == ast.BinaryOperationType.Add: @@ -332,10 +337,14 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): except ResolverException: symbol_with_name_in_scope = None - if symbol_with_name_in_scope == symbol: - field_sql = printed_field - else: + if ( + symbol_with_name_in_scope != symbol + or isinstance(symbol.table, ast.TableAliasSymbol) + or isinstance(symbol.table, ast.SelectQueryAliasSymbol) + ): field_sql = f"{table_prefix}.{printed_field}" + else: + field_sql = printed_field if printed_field != "properties": # TODO: refactor this property access logging diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index f2be08fdbadf1..94a7d5b510507 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -178,9 +178,7 @@ def visit_constant(self, node): def lookup_field_by_name(scope: ast.SelectQuerySymbol, name: str) -> Optional[ast.Symbol]: - if name in scope.columns: - return scope.columns[name] - elif name in scope.aliases: + if name in scope.aliases: return scope.aliases[name] else: named_tables = [table for table in scope.tables.values() if table.has_child(name)] diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 27ad2b99f6e03..dc329218df99b 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -91,7 +91,7 @@ def test_query_joins_simple(self): response = execute_hogql_query( """ - SELECT event, timestamp, e.distinct_id, p.id, p.properties.email + SELECT event, timestamp, pdi.distinct_id, p.id, p.properties.email FROM events e LEFT JOIN person_distinct_id pdi ON pdi.distinct_id = e.distinct_id @@ -102,8 +102,15 @@ def test_query_joins_simple(self): ) self.assertEqual( response.clickhouse, - f"", + f"SELECT e.event, e.timestamp, pdi.distinct_id, p.id, replaceRegexpAll(JSONExtractRaw(p.properties, %(hogql_val_0)s), '^\"|\"$', '') FROM events AS e LEFT JOIN person_distinct_id2 AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN person AS p ON equals(p.id, pdi.person_id) WHERE and(equals(e.team_id, {self.team.id}), equals(pdi.team_id, {self.team.id}), equals(p.team_id, {self.team.id})) LIMIT 1000", ) + self.assertEqual( + response.hogql, + "SELECT event, timestamp, pdi.distinct_id, p.id, p.properties.email FROM events AS e LEFT JOIN person_distinct_id2 AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN person AS p ON equals(p.id, pdi.person_id) LIMIT 1000", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][4], "tim@posthog.com") def test_query_joins(self): with freeze_time("2020-01-10"): From 95b6a5e240126180b1e217c7c2386ef4026cbb17 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 13 Feb 2023 23:31:06 +0100 Subject: [PATCH 029/142] query aliases --- posthog/hogql/ast.py | 27 ++++++++++++++------------- posthog/hogql/printer.py | 7 +++---- posthog/hogql/test/test_query.py | 24 +++++++++++++++++++----- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d50181ba1196c..d69f70d271db1 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -45,17 +45,6 @@ def has_child(self, name: str) -> bool: return self.symbol.has_child(name) -class SelectQueryAliasSymbol(Symbol): - name: str - symbol: Symbol - - def get_child(self, name: str) -> Symbol: - return self.symbol.get_child(name) - - def has_child(self, name: str) -> bool: - return self.symbol.has_child(name) - - class TableSymbol(Symbol): table: Table @@ -81,6 +70,18 @@ def get_child(self, name: str) -> Symbol: return self.table.get_child(name) +class SelectQueryAliasSymbol(Symbol): + name: str + symbol: Symbol + + def get_child(self, name: str) -> Optional[Symbol]: + if self.symbol.has_child(name): + return FieldSymbol(name=name, table=self) + + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) + + class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope aliases: Dict[str, ColumnAliasSymbol] @@ -93,7 +94,7 @@ class SelectQuerySymbol(Symbol): def get_child(self, name: str) -> Symbol: if name in self.columns: - return self.columns[name] + return FieldSymbol(name=name, table=self) raise NotImplementedError(f"Column not found: {name}") def has_child(self, name: str) -> bool: @@ -110,7 +111,7 @@ class CallSymbol(Symbol): class FieldSymbol(Symbol): name: str - table: Union[TableSymbol, TableAliasSymbol] + table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] def get_child(self, name: str) -> Symbol: table_symbol = self.table diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 3bdffc29d0804..02060de6f2153 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -77,9 +77,9 @@ def visit_select_query(self, node: ast.SelectQuery): if where is None: where = extra_where elif isinstance(where, ast.And): - where = ast.And(exprs=where.exprs + [extra_where]) + where = ast.And(exprs=[extra_where] + where.exprs) else: - where = ast.And(exprs=[where, extra_where]) + where = ast.And(exprs=[extra_where, where]) next_join = next_join.next_join @@ -328,8 +328,6 @@ def visit_table_alias_symbol(self, symbol: ast.TableAliasSymbol): return print_hogql_identifier(symbol.name) def visit_field_symbol(self, symbol: ast.FieldSymbol): - # do we need a table prefix? - table_prefix = self.visit(symbol.table) printed_field = print_hogql_identifier(symbol.name) try: @@ -342,6 +340,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): or isinstance(symbol.table, ast.TableAliasSymbol) or isinstance(symbol.table, ast.SelectQueryAliasSymbol) ): + table_prefix = self.visit(symbol.table) field_sql = f"{table_prefix}.{printed_field}" else: field_sql = printed_field diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index dc329218df99b..f39da1e54b93b 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -43,13 +43,27 @@ def test_query(self): ) self.assertEqual(response.results, [(2, "random event")]) + response = execute_hogql_query( + f"select count, event from (select count() as count, event from events where properties.random_uuid = '{random_uuid}' group by event) group by count, event", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT count, event FROM (SELECT count(*) AS count, event FROM events WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', ''), %(hogql_val_1)s)) GROUP BY event) GROUP BY count, event LIMIT 1000", + ) + self.assertEqual( + response.hogql, + "SELECT count, event FROM (SELECT count() AS count, event FROM events WHERE equals(properties.random_uuid, %(hogql_val_2)s) GROUP BY event) GROUP BY count, event LIMIT 1000", + ) + self.assertEqual(response.results, [(2, "random event")]) + response = execute_hogql_query( f"select count, event from (select count() as count, event from events where properties.random_uuid = '{random_uuid}' group by event) as c group by count, event", self.team, ) self.assertEqual( response.clickhouse, - f"SELECT count, event FROM (SELECT count(*) AS count, event FROM events WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', ''), %(hogql_val_1)s)) GROUP BY event) AS c GROUP BY count, event LIMIT 1000", + f"SELECT c.count, c.event FROM (SELECT count(*) AS count, event FROM events WHERE and(equals(team_id, {self.team.id}), equals(replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', ''), %(hogql_val_1)s)) GROUP BY event) AS c GROUP BY c.count, c.event LIMIT 1000", ) self.assertEqual( response.hogql, @@ -102,7 +116,7 @@ def test_query_joins_simple(self): ) self.assertEqual( response.clickhouse, - f"SELECT e.event, e.timestamp, pdi.distinct_id, p.id, replaceRegexpAll(JSONExtractRaw(p.properties, %(hogql_val_0)s), '^\"|\"$', '') FROM events AS e LEFT JOIN person_distinct_id2 AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN person AS p ON equals(p.id, pdi.person_id) WHERE and(equals(e.team_id, {self.team.id}), equals(pdi.team_id, {self.team.id}), equals(p.team_id, {self.team.id})) LIMIT 1000", + f"SELECT e.event, e.timestamp, pdi.distinct_id, p.id, replaceRegexpAll(JSONExtractRaw(p.properties, %(hogql_val_0)s), '^\"|\"$', '') FROM events AS e LEFT JOIN person_distinct_id2 AS pdi ON equals(pdi.distinct_id, e.distinct_id) LEFT JOIN person AS p ON equals(p.id, pdi.person_id) WHERE and(equals(p.team_id, {self.team.id}), equals(pdi.team_id, {self.team.id}), equals(e.team_id, {self.team.id})) LIMIT 1000", ) self.assertEqual( response.hogql, @@ -121,19 +135,19 @@ def test_query_joins(self): SELECT distinct_id, argMax(person_id, version) as person_id FROM person_distinct_id - WHERE team_id = 1 GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0 ) AS pdi ON e.distinct_id = pdi.distinct_id""", self.team, ) + self.assertEqual( response.clickhouse, - f"SELECT DISTINCT person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) LIMIT 1000", + f"SELECT e.event, e.timestamp, pdi.person_id FROM events AS e INNER JOIN (SELECT distinct_id, argMax(person_distinct_id2.person_id, version) AS person_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) WHERE equals(e.team_id, {self.team.id}) LIMIT 1000", ) self.assertEqual( response.hogql, - "SELECT DISTINCT person_id, distinct_id FROM person_distinct_id LIMIT 1000", + "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_id2 GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) LIMIT 1000", ) self.assertTrue(len(response.results) > 0) From da83eec5da74c1ea0531e021c231fa495dbe060d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 00:26:15 +0100 Subject: [PATCH 030/142] errors, cleanup, recordings --- posthog/hogql/ast.py | 26 ++++++++++++------------ posthog/hogql/database.py | 31 +++++++++++++++++++++++++++-- posthog/hogql/printer.py | 4 ++-- posthog/hogql/test/test_printer.py | 5 +++-- posthog/hogql/test/test_query.py | 6 +++--- posthog/hogql/test/test_resolver.py | 12 +++++------ 6 files changed, 56 insertions(+), 28 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d69f70d271db1..a93e988d79d27 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -54,7 +54,7 @@ def has_child(self, name: str) -> bool: def get_child(self, name: str) -> Symbol: if self.has_child(name): return FieldSymbol(name=name, table=self) - raise NotImplementedError(f"Field not found: {name}") + raise ValueError(f"Field not found: {name}") class TableAliasSymbol(Symbol): @@ -74,9 +74,10 @@ class SelectQueryAliasSymbol(Symbol): name: str symbol: Symbol - def get_child(self, name: str) -> Optional[Symbol]: + def get_child(self, name: str) -> Symbol: if self.symbol.has_child(name): return FieldSymbol(name=name, table=self) + raise ValueError(f"Field not found: {name}") def has_child(self, name: str) -> bool: return self.symbol.has_child(name) @@ -95,7 +96,7 @@ class SelectQuerySymbol(Symbol): def get_child(self, name: str) -> Symbol: if name in self.columns: return FieldSymbol(name=name, table=self) - raise NotImplementedError(f"Column not found: {name}") + raise ValueError(f"Column not found: {name}") def has_child(self, name: str) -> bool: return name in self.columns @@ -117,16 +118,15 @@ def get_child(self, name: str) -> Symbol: table_symbol = self.table while isinstance(table_symbol, TableAliasSymbol): table_symbol = table_symbol.table - db_table = table_symbol.table - if isinstance(db_table, Table): - if self.name in db_table.__fields__ and isinstance(db_table.__fields__[self.name].default, StringJSONValue): - return PropertySymbol(name=name, field=self) - else: - raise NotImplementedError( - f"Can not access property {name} on field {self.name} because it's not a JSON field" - ) - else: - raise NotImplementedError(f"Can not resolve table for field: {self.name}") + + if isinstance(table_symbol, TableSymbol): + db_table = table_symbol.table + if isinstance(db_table, Table): + if self.name in db_table.__fields__ and isinstance( + db_table.__fields__[self.name].default, StringJSONValue + ): + return PropertySymbol(name=name, field=self) + raise ValueError(f"Can not access property {name} on field {self.name}.") class ConstantSymbol(Symbol): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 031f63502f0ba..4cf5eb639ffb3 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -26,6 +26,10 @@ class BooleanValue(Field): pass +class ArrayValue(Field): + field: Field + + class Table(BaseModel): class Config: extra = Extra.forbid @@ -79,6 +83,28 @@ def clickhouse_table(self): return "events" +class SessionRecordingEvents(Table): + uuid: StringValue = StringValue() + timestamp: DateTimeValue = DateTimeValue() + team_id: IntegerValue = IntegerValue() + distinct_id: StringValue = StringValue() + session_id: StringValue = StringValue() + window_id: StringValue = StringValue() + snapshot_data: StringValue = StringValue() + created_at: DateTimeValue = DateTimeValue() + has_full_snapshot: BooleanValue = BooleanValue() + events_summary: ArrayValue = ArrayValue(field=BooleanValue()) + click_count: IntegerValue = IntegerValue() + keypress_count: IntegerValue = IntegerValue() + timestamps_summary: ArrayValue = ArrayValue(field=DateTimeValue()) + first_event_timestamp: DateTimeValue = DateTimeValue() + last_event_timestamp: DateTimeValue = DateTimeValue() + urls: ArrayValue = ArrayValue(field=StringValue()) + + def clickhouse_table(self): + return "session_recording_events" + + # class NumbersTable(Table): # args: [IntegerValue, IntegerValue] @@ -87,10 +113,11 @@ class Database(BaseModel): class Config: extra = Extra.forbid - # All fields below will be tables users can query from + # Users can query from the tables below events: EventsTable = EventsTable() persons: PersonsTable = PersonsTable() - person_distinct_id: PersonDistinctIdTable = PersonDistinctIdTable() + person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() + session_recording_events: SessionRecordingEvents = SessionRecordingEvents() # numbers: NumbersTable = NumbersTable() diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 02060de6f2153..200edbd18ad51 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1,4 +1,4 @@ -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional, Tuple, Union from ee.clickhouse.materialized_columns.columns import get_materialized_columns from posthog.hogql import ast @@ -123,7 +123,7 @@ def visit_select_query(self, node: ast.SelectQuery): response = f"({response})" return response - def visit_join_expr(self, node: ast.JoinExpr) -> (str, Optional[ast.Expr]): + def visit_join_expr(self, node: ast.JoinExpr) -> Tuple[str, Optional[ast.Expr]]: # return constraints we must place on the select query extra_where = None diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index fe082d123cc22..bf1a055ee474b 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -370,7 +370,8 @@ def test_select_alias(self): # currently not supported! self.assertEqual(self._select("select 1 as b"), "SELECT 1 AS b LIMIT 65535") self.assertEqual( - self._select("select 1 from events as e"), "SELECT 1 FROM events AS e WHERE equals(team_id, 42) LIMIT 65535" + self._select("select 1 from events as e"), + "SELECT 1 FROM events AS e WHERE equals(e.team_id, 42) LIMIT 65535", ) def test_select_from(self): @@ -469,5 +470,5 @@ def test_select_subquery(self): ) self.assertEqual( self._select("SELECT event from (select distinct event from events group by event, timestamp) e"), - "SELECT event FROM (SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp) AS e LIMIT 65535", + "SELECT e.event FROM (SELECT DISTINCT event FROM events WHERE equals(team_id, 42) GROUP BY event, timestamp) AS e LIMIT 65535", ) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index f39da1e54b93b..c1cefa49361f0 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -86,7 +86,7 @@ def test_query(self): self.assertEqual(response.results, [("tim@posthog.com",)]) response = execute_hogql_query( - f"select distinct person_id, distinct_id from person_distinct_id", + f"select distinct person_id, distinct_id from person_distinct_ids", self.team, ) self.assertEqual( @@ -107,7 +107,7 @@ def test_query_joins_simple(self): """ SELECT event, timestamp, pdi.distinct_id, p.id, p.properties.email FROM events e - LEFT JOIN person_distinct_id pdi + LEFT JOIN person_distinct_ids pdi ON pdi.distinct_id = e.distinct_id LEFT JOIN persons p ON p.id = pdi.person_id @@ -134,7 +134,7 @@ def test_query_joins(self): """select event, timestamp, pdi.person_id from events e INNER JOIN ( SELECT distinct_id, argMax(person_id, version) as person_id - FROM person_distinct_id + FROM person_distinct_ids GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0 ) AS pdi diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 0c522322151fe..cb7e8c1927d85 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -55,7 +55,7 @@ def test_resolve_events_table_alias(self): select_query_symbol = ast.SelectQuerySymbol( aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, - tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, + tables={"e": ast.TableAliasSymbol(name="e", table=events_table_symbol)}, anonymous_tables=[], ) @@ -67,7 +67,7 @@ def test_resolve_events_table_alias(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="e", - symbol=ast.TableAliasSymbol(name="e", symbol=events_table_symbol), + symbol=ast.TableAliasSymbol(name="e", table=events_table_symbol), ), where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), @@ -102,7 +102,7 @@ def test_resolve_events_table_column_alias(self): "e": ast.ColumnAliasSymbol(name="e", symbol=event_field_symbol), "timestamp": timestamp_field_symbol, }, - tables={"e": ast.TableAliasSymbol(name="e", symbol=events_table_symbol)}, + tables={"e": ast.TableAliasSymbol(name="e", table=events_table_symbol)}, anonymous_tables=[], ) @@ -144,7 +144,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") resolve_symbols(expr) outer_events_table_symbol = ast.TableSymbol(table=database.events) - inner_events_table_symbol = ast.TableAliasSymbol(name="e1", symbol=ast.TableSymbol(table=database.events)) + inner_events_table_symbol = ast.TableAliasSymbol(name="e1", table=ast.TableSymbol(table=database.events)) outer_event_field_symbol = ast.FieldSymbol(name="event", table=outer_events_table_symbol) inner_event_field_symbol = ast.FieldSymbol(name="event", table=inner_events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=outer_events_table_symbol) @@ -200,7 +200,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): symbol=inner_select_symbol, ), alias="e", - symbol=ast.TableAliasSymbol(name="e", symbol=inner_select_symbol), + symbol=ast.TableAliasSymbol(name="e", table=inner_select_symbol), ), where=ast.CompareOperation( left=ast.Field( @@ -218,7 +218,7 @@ def test_resolve_events_table_column_alias_inside_subquery(self): columns={ "b": ast.ColumnAliasSymbol(name="b", symbol=outer_event_field_symbol), }, - tables={"e": ast.TableAliasSymbol(name="e", symbol=inner_select_symbol)}, + tables={"e": ast.TableAliasSymbol(name="e", table=inner_select_symbol)}, ), ) # asserting individually to help debug if something is off From 2e27c1e82d0c992808e1bb45a997b4fb11a6f71c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 01:20:23 +0100 Subject: [PATCH 031/142] resolver tests --- posthog/hogql/resolver.py | 6 ++- posthog/hogql/test/test_resolver.py | 78 ++++++++++++++--------------- 2 files changed, 41 insertions(+), 43 deletions(-) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 94a7d5b510507..459acb24c68f2 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -83,9 +83,11 @@ def visit_join_expr(self, node): if table_name in database.__fields__: table = database.__fields__[table_name].default - node.symbol = ast.TableSymbol(table=table) + node.table.symbol = ast.TableSymbol(table=table) if table_alias != table_name: - node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.symbol) + node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.table.symbol) + else: + node.symbol = node.table.symbol scope.tables[table_alias] = node.symbol else: raise ResolverException(f'Unknown table "{table_name}".') diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index cb7e8c1927d85..bb8ec23d68e90 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -27,7 +27,6 @@ def test_resolve_events_table(self): ], select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), - alias="events", symbol=events_table_symbol, ), where=ast.CompareOperation( @@ -50,12 +49,13 @@ def test_resolve_events_table_alias(self): resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) - timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + events_table_alias_symbol = ast.TableAliasSymbol(name="e", table=events_table_symbol) + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_alias_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) select_query_symbol = ast.SelectQuerySymbol( aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, - tables={"e": ast.TableAliasSymbol(name="e", table=events_table_symbol)}, + tables={"e": events_table_alias_symbol}, anonymous_tables=[], ) @@ -67,7 +67,7 @@ def test_resolve_events_table_alias(self): select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=events_table_symbol), alias="e", - symbol=ast.TableAliasSymbol(name="e", table=events_table_symbol), + symbol=events_table_alias_symbol, ), where=ast.CompareOperation( left=ast.Field(chain=["e", "event"], symbol=event_field_symbol), @@ -89,20 +89,25 @@ def test_resolve_events_table_column_alias(self): resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) - timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) + events_table_alias_symbol = ast.TableAliasSymbol(name="e", table=events_table_symbol) + event_field_symbol = ast.FieldSymbol(name="event", table=events_table_alias_symbol) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) select_query_symbol = ast.SelectQuerySymbol( aliases={ "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.ColumnAliasSymbol(name="e", symbol=event_field_symbol), + "e": ast.ColumnAliasSymbol( + name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) + ), }, columns={ "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.ColumnAliasSymbol(name="e", symbol=event_field_symbol), + "e": ast.ColumnAliasSymbol( + name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) + ), "timestamp": timestamp_field_symbol, }, - tables={"e": ast.TableAliasSymbol(name="e", table=events_table_symbol)}, + tables={"e": events_table_alias_symbol}, anonymous_tables=[], ) @@ -117,7 +122,7 @@ def test_resolve_events_table_column_alias(self): ast.Alias( alias="e", expr=ast.Field(chain=["ee"], symbol=select_query_symbol.aliases["ee"]), - symbol=select_query_symbol.aliases["e"], + symbol=select_query_symbol.aliases["e"], # is ee ? ), ast.Field(chain=["e", "timestamp"], symbol=timestamp_field_symbol), ], @@ -143,32 +148,34 @@ def test_resolve_events_table_column_alias(self): def test_resolve_events_table_column_alias_inside_subquery(self): expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") resolve_symbols(expr) - outer_events_table_symbol = ast.TableSymbol(table=database.events) - inner_events_table_symbol = ast.TableAliasSymbol(name="e1", table=ast.TableSymbol(table=database.events)) - outer_event_field_symbol = ast.FieldSymbol(name="event", table=outer_events_table_symbol) - inner_event_field_symbol = ast.FieldSymbol(name="event", table=inner_events_table_symbol) - timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=outer_events_table_symbol) + inner_events_table_symbol = ast.TableSymbol(table=database.events) + inner_event_field_symbol = ast.ColumnAliasSymbol( + name="b", symbol=ast.FieldSymbol(name="event", table=inner_events_table_symbol) + ) + timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=inner_events_table_symbol) + timstamp_alias_symbol = ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol) inner_select_symbol = ast.SelectQuerySymbol( aliases={ - "b": ast.ColumnAliasSymbol(name="b", symbol=outer_event_field_symbol), + "b": inner_event_field_symbol, "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), }, columns={ - "b": ast.ColumnAliasSymbol(name="b", symbol=outer_events_table_symbol), + "b": inner_event_field_symbol, "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), }, tables={ - "events": outer_events_table_symbol, + "events": inner_events_table_symbol, }, anonymous_tables=[], ) + select_alias_symbol = ast.SelectQueryAliasSymbol(name="e", symbol=inner_select_symbol) expected = ast.SelectQuery( select=[ ast.Field( chain=["b"], - symbol=ast.ColumnAliasSymbol( + symbol=ast.FieldSymbol( name="b", - symbol=outer_event_field_symbol, + table=ast.SelectQueryAliasSymbol(name="e", symbol=inner_select_symbol), ), ), ], @@ -177,48 +184,37 @@ def test_resolve_events_table_column_alias_inside_subquery(self): select=[ ast.Alias( alias="b", - expr=ast.Field(chain=["event"], symbol=inner_event_field_symbol), - symbol=ast.ColumnAliasSymbol( - name="b", - symbol=inner_event_field_symbol, - ), + expr=ast.Field(chain=["event"], symbol=inner_event_field_symbol.symbol), + symbol=inner_event_field_symbol, ), ast.Alias( alias="c", expr=ast.Field(chain=["timestamp"], symbol=timestamp_field_symbol), - symbol=ast.ColumnAliasSymbol( - name="c", - symbol=timestamp_field_symbol, - ), + symbol=timstamp_alias_symbol, ), ], select_from=ast.JoinExpr( table=ast.Field(chain=["events"], symbol=inner_events_table_symbol), - alias="events", symbol=inner_events_table_symbol, ), symbol=inner_select_symbol, ), alias="e", - symbol=ast.TableAliasSymbol(name="e", table=inner_select_symbol), + symbol=select_alias_symbol, ), where=ast.CompareOperation( left=ast.Field( chain=["e", "b"], - symbol=ast.ColumnAliasSymbol( - name="b", - symbol=outer_event_field_symbol, - ), + symbol=ast.FieldSymbol(name="b", table=select_alias_symbol), ), op=ast.CompareOperationType.Eq, right=ast.Constant(value="test", symbol=ast.ConstantSymbol(value="test")), ), symbol=ast.SelectQuerySymbol( aliases={}, - columns={ - "b": ast.ColumnAliasSymbol(name="b", symbol=outer_event_field_symbol), - }, - tables={"e": ast.TableAliasSymbol(name="e", table=inner_select_symbol)}, + columns={"b": ast.FieldSymbol(name="b", table=select_alias_symbol)}, + tables={"e": select_alias_symbol}, + anonymous_tables=[], ), ) # asserting individually to help debug if something is off @@ -248,7 +244,7 @@ def test_resolve_errors(self): for query in queries: with self.assertRaises(ResolverException) as e: resolve_symbols(parse_select(query)) - self.assertIn("Unable to resolve field: x", str(e.exception)) + self.assertIn("Unable to resolve field:", str(e.exception)) # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" From 78caa571766742f65dd3fb6da2709f7ec2bf0331 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 11:45:18 +0100 Subject: [PATCH 032/142] cleanup --- posthog/hogql/ast.py | 44 ++++++++++++++++------------- posthog/hogql/hogql.py | 2 +- posthog/hogql/printer.py | 32 ++++++++------------- posthog/hogql/resolver.py | 17 +++++------ posthog/hogql/test/test_resolver.py | 27 ++++++------------ 5 files changed, 54 insertions(+), 68 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index a93e988d79d27..ee4854ecbe202 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -3,10 +3,11 @@ from typing import Any, Dict, List, Literal, Optional, Union from pydantic import BaseModel, Extra +from pydantic import Field as PydanticField from posthog.hogql.database import StringJSONValue, Table -# NOTE: when you add new AST fields or nodes, add them to CloningVisitor as well! +# NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! camel_case_pattern = re.compile(r"(? bool: return self.get_child(name) is not None -class ColumnAliasSymbol(Symbol): +class FieldAliasSymbol(Symbol): name: str symbol: Symbol @@ -70,28 +71,17 @@ def get_child(self, name: str) -> Symbol: return self.table.get_child(name) -class SelectQueryAliasSymbol(Symbol): - name: str - symbol: Symbol - - def get_child(self, name: str) -> Symbol: - if self.symbol.has_child(name): - return FieldSymbol(name=name, table=self) - raise ValueError(f"Field not found: {name}") - - def has_child(self, name: str) -> bool: - return self.symbol.has_child(name) - - class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope - aliases: Dict[str, ColumnAliasSymbol] + aliases: Dict[str, FieldAliasSymbol] = PydanticField(default_factory=dict) # all symbols a select query exports - columns: Dict[str, Symbol] + columns: Dict[str, Symbol] = PydanticField(default_factory=dict) # all from and join, tables and subqueries with aliases - tables: Dict[str, Union[TableSymbol, TableAliasSymbol, "SelectQuerySymbol", SelectQueryAliasSymbol]] + tables: Dict[ + str, Union[TableSymbol, TableAliasSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"] + ] = PydanticField(default_factory=dict) # all from and join subqueries without aliases - anonymous_tables: List["SelectQuerySymbol"] + anonymous_tables: List["SelectQuerySymbol"] = PydanticField(default_factory=list) def get_child(self, name: str) -> Symbol: if name in self.columns: @@ -102,7 +92,21 @@ def has_child(self, name: str) -> bool: return name in self.columns +class SelectQueryAliasSymbol(Symbol): + name: str + symbol: SelectQuerySymbol + + def get_child(self, name: str) -> Symbol: + if self.symbol.has_child(name): + return FieldSymbol(name=name, table=self) + raise ValueError(f"Field not found: {name}") + + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) + + SelectQuerySymbol.update_forward_refs(SelectQuerySymbol=SelectQuerySymbol) +SelectQuerySymbol.update_forward_refs(SelectQueryAliasSymbol=SelectQueryAliasSymbol) class CallSymbol(Symbol): diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 5a10fae468d51..c6e2c1a962e0d 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -21,7 +21,7 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", else: node = parse_expr(query, no_placeholders=True) symbol = ast.SelectQuerySymbol( - aliases={}, columns={}, tables={"events": ast.TableSymbol(table=database.events)}, anonymous_tables=[] + tables={"events": ast.TableSymbol(table=database.events)}, ) resolve_symbols(node, symbol) return print_ast(node, context, dialect, stack=[ast.SelectQuery(select=[], symbol=symbol)]) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 200edbd18ad51..47797080192db 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -9,27 +9,17 @@ from posthog.hogql.visitor import Visitor -def guard_where_team_id( - where: Optional[ast.Expr], table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbol], context: HogQLContext -) -> ast.Expr: +def guard_where_team_id(table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbol], context: HogQLContext) -> ast.Expr: """Add a mandatory "and(team_id, ...)" filter around the expression.""" if not context.select_team_id: raise ValueError("context.select_team_id not found") - team_clause = ast.CompareOperation( + return ast.CompareOperation( op=ast.CompareOperationType.Eq, left=ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=table_symbol)), right=ast.Constant(value=context.select_team_id), ) - if isinstance(where, ast.And): - where = ast.And(exprs=[team_clause] + where.exprs) - elif where: - where = ast.And(exprs=[team_clause, where]) - else: - where = team_clause - return where - def print_ast( node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: List[ast.AST] = [] @@ -142,13 +132,13 @@ def visit_join_expr(self, node: ast.JoinExpr) -> Tuple[str, Optional[ast.Expr]]: select_from.append(f"AS {print_hogql_identifier(node.alias)}") if self.dialect == "clickhouse": - extra_where = guard_where_team_id(None, node.symbol, self.context) + extra_where = guard_where_team_id(node.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) if self.dialect == "clickhouse": - extra_where = guard_where_team_id(None, node.symbol, self.context) + extra_where = guard_where_team_id(node.symbol, self.context) elif isinstance(node.symbol, ast.SelectQuerySymbol): select_from.append(self.visit(node.table)) @@ -256,12 +246,14 @@ def visit_field(self, node: ast.Field): if self.dialect == "hogql": # When printing HogQL, we print the properties out as a chain instead of converting them to Clickhouse SQL return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) - # elif node.chain == ["*"]: - # query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" - # return self.visit(parse_expr(query)) - # elif node.chain == ["person"]: - # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" - # return self.visit(parse_expr(query)) + elif node.chain == ["*"]: + raise ValueError("Selecting * not implemented") + # query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" + # return self.visit(parse_expr(query)) + elif node.chain == ["person"]: + raise ValueError("Selecting person not implemented") + # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" + # return self.visit(parse_expr(query)) elif node.symbol is not None: select_query = self._last_select() select: Optional[ast.SelectQuerySymbol] = select_query.symbol if select_query else None diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 459acb24c68f2..8c76bf9c58f24 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -19,6 +19,7 @@ class Resolver(TraversingVisitor): """The Resolver visits an AST and assigns Symbols to the nodes.""" def __init__(self, scope: Optional[ast.SelectQuerySymbol] = None): + # Each SELECT query creates a new scope. Store all of them in a list as we traverse the tree. self.scopes: List[ast.SelectQuerySymbol] = [scope] if scope else [] def visit_select_query(self, node): @@ -26,7 +27,7 @@ def visit_select_query(self, node): if node.symbol is not None: return - node.symbol = ast.SelectQuerySymbol(aliases={}, columns={}, tables={}, anonymous_tables=[]) + node.symbol = ast.SelectQuerySymbol() # Each SELECT query creates a new scope. Store all of them in a list for variable access. self.scopes.append(node.symbol) @@ -40,7 +41,7 @@ def visit_select_query(self, node): # SELECT e.event, e.timestamp from (SELECT event, timestamp FROM events) AS e for expr in node.select or []: self.visit(expr) - if isinstance(expr.symbol, ast.ColumnAliasSymbol): + if isinstance(expr.symbol, ast.FieldAliasSymbol): node.symbol.columns[expr.symbol.name] = expr.symbol elif isinstance(expr, ast.Alias): node.symbol.columns[expr.alias] = expr.symbol @@ -125,7 +126,7 @@ def visit_alias(self, node: ast.Alias): self.visit(node.expr) if not node.expr.symbol: raise ResolverException(f"Cannot alias an expression without a symbol: {node.alias}") - node.symbol = ast.ColumnAliasSymbol(name=node.alias, symbol=node.expr.symbol) + node.symbol = ast.FieldAliasSymbol(name=node.alias, symbol=node.expr.symbol) scope.aliases[node.alias] = node.symbol def visit_call(self, node: ast.Call): @@ -144,12 +145,12 @@ def visit_field(self, node): raise Exception("Invalid field access with empty chain") # ClickHouse does not support subqueries accessing "x.event" like this: - # "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", - # + # - "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", # But this is supported: - # "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t JOIN events e ON (e.event = t.event)", + # - "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t JOIN events e ON (e.event = t.event)", # - # Thus only look into the current scope, for columns and aliases. + # Thus we only look into scopes[-1] to see aliases in the current scope, and don't loop over all the scopes. + scope = self.scopes[-1] symbol: Optional[ast.Symbol] = None name = node.chain[0] @@ -180,6 +181,7 @@ def visit_constant(self, node): def lookup_field_by_name(scope: ast.SelectQuerySymbol, name: str) -> Optional[ast.Symbol]: + """Looks for a field in the scope's list of aliases and children for each joined table.""" if name in scope.aliases: return scope.aliases[name] else: @@ -190,6 +192,5 @@ def lookup_field_by_name(scope: ast.SelectQuerySymbol, name: str) -> Optional[as if len(tables) > 1: raise ResolverException(f"Ambiguous query. Found multiple sources for field: {name}") elif len(tables) == 1: - # accessed a field on a joined table by name return tables[0].get_child(name) return None diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index bb8ec23d68e90..8e4fd16cf095a 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -14,10 +14,8 @@ def test_resolve_events_table(self): event_field_symbol = ast.FieldSymbol(name="event", table=events_table_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_symbol) select_query_symbol = ast.SelectQuerySymbol( - aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, tables={"events": events_table_symbol}, - anonymous_tables=[], ) expected = ast.SelectQuery( @@ -53,10 +51,8 @@ def test_resolve_events_table_alias(self): event_field_symbol = ast.FieldSymbol(name="event", table=events_table_alias_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) select_query_symbol = ast.SelectQuerySymbol( - aliases={}, columns={"event": event_field_symbol, "timestamp": timestamp_field_symbol}, tables={"e": events_table_alias_symbol}, - anonymous_tables=[], ) expected = ast.SelectQuery( @@ -95,20 +91,15 @@ def test_resolve_events_table_column_alias(self): select_query_symbol = ast.SelectQuerySymbol( aliases={ - "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.ColumnAliasSymbol( - name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) - ), + "ee": ast.FieldAliasSymbol(name="ee", symbol=event_field_symbol), + "e": ast.FieldAliasSymbol(name="e", symbol=ast.FieldAliasSymbol(name="ee", symbol=event_field_symbol)), }, columns={ - "ee": ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol), - "e": ast.ColumnAliasSymbol( - name="e", symbol=ast.ColumnAliasSymbol(name="ee", symbol=event_field_symbol) - ), + "ee": ast.FieldAliasSymbol(name="ee", symbol=event_field_symbol), + "e": ast.FieldAliasSymbol(name="e", symbol=ast.FieldAliasSymbol(name="ee", symbol=event_field_symbol)), "timestamp": timestamp_field_symbol, }, tables={"e": events_table_alias_symbol}, - anonymous_tables=[], ) expected = ast.SelectQuery( @@ -149,24 +140,23 @@ def test_resolve_events_table_column_alias_inside_subquery(self): expr = parse_select("SELECT b FROM (select event as b, timestamp as c from events) e WHERE e.b = 'test'") resolve_symbols(expr) inner_events_table_symbol = ast.TableSymbol(table=database.events) - inner_event_field_symbol = ast.ColumnAliasSymbol( + inner_event_field_symbol = ast.FieldAliasSymbol( name="b", symbol=ast.FieldSymbol(name="event", table=inner_events_table_symbol) ) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=inner_events_table_symbol) - timstamp_alias_symbol = ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol) + timstamp_alias_symbol = ast.FieldAliasSymbol(name="c", symbol=timestamp_field_symbol) inner_select_symbol = ast.SelectQuerySymbol( aliases={ "b": inner_event_field_symbol, - "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), + "c": ast.FieldAliasSymbol(name="c", symbol=timestamp_field_symbol), }, columns={ "b": inner_event_field_symbol, - "c": ast.ColumnAliasSymbol(name="c", symbol=timestamp_field_symbol), + "c": ast.FieldAliasSymbol(name="c", symbol=timestamp_field_symbol), }, tables={ "events": inner_events_table_symbol, }, - anonymous_tables=[], ) select_alias_symbol = ast.SelectQueryAliasSymbol(name="e", symbol=inner_select_symbol) expected = ast.SelectQuery( @@ -214,7 +204,6 @@ def test_resolve_events_table_column_alias_inside_subquery(self): aliases={}, columns={"b": ast.FieldSymbol(name="b", table=select_alias_symbol)}, tables={"e": select_alias_symbol}, - anonymous_tables=[], ), ) # asserting individually to help debug if something is off From 48f684fc1b629ee2b4c69714d26d4024c48a26aa Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 12:24:12 +0100 Subject: [PATCH 033/142] more tiny cleanup --- posthog/hogql/constants.py | 2 +- posthog/hogql/database.py | 5 ----- posthog/hogql/hogql.py | 10 ++++++---- posthog/hogql/parser.py | 6 +++--- 4 files changed, 10 insertions(+), 13 deletions(-) diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 0a42c31f7d500..92c88a8c65df4 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -98,7 +98,7 @@ KEYWORDS = ["true", "false", "null"] # Keywords you can't alias to -RESERVED_KEYWORDS = ["team_id"] +RESERVED_KEYWORDS = KEYWORDS + ["team_id"] # Allow-listed fields returned when you select "*" from events. Person and group fields will be nested later. SELECT_STAR_FROM_EVENTS_FIELDS = [ diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 4cf5eb639ffb3..c9fa18fb84a30 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -105,10 +105,6 @@ def clickhouse_table(self): return "session_recording_events" -# class NumbersTable(Table): -# args: [IntegerValue, IntegerValue] - - class Database(BaseModel): class Config: extra = Extra.forbid @@ -118,7 +114,6 @@ class Config: persons: PersonsTable = PersonsTable() person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() - # numbers: NumbersTable = NumbersTable() database = Database() diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index c6e2c1a962e0d..541d0199b4c1f 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -15,16 +15,18 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", try: if context.select_team_id: + # Only parse full SELECT statements if we have a team_id in the context. node = parse_select(query, no_placeholders=True) resolve_symbols(node) return print_ast(node, context, dialect, stack=[]) else: + # Create a fake query that selects from "events". Assume were in its scope when evaluating expressions. + symbol = ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) + select_query = ast.SelectQuery(select=[], symbol=symbol) + node = parse_expr(query, no_placeholders=True) - symbol = ast.SelectQuerySymbol( - tables={"events": ast.TableSymbol(table=database.events)}, - ) resolve_symbols(node, symbol) - return print_ast(node, context, dialect, stack=[ast.SelectQuery(select=[], symbol=symbol)]) + return print_ast(node, context, dialect, stack=[select_query]) except SyntaxError as err: raise ValueError(f"SyntaxError: {err.msg}") diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index f0893f9aa6eb5..5b7f4b9eff6fc 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -4,7 +4,7 @@ from antlr4.error.ErrorListener import ErrorListener from posthog.hogql import ast -from posthog.hogql.constants import KEYWORDS, RESERVED_KEYWORDS +from posthog.hogql.constants import RESERVED_KEYWORDS from posthog.hogql.grammar.HogQLLexer import HogQLLexer from posthog.hogql.grammar.HogQLParser import HogQLParser from posthog.hogql.parse_string import parse_string, parse_string_literal @@ -318,7 +318,7 @@ def visitColumnExprAlias(self, ctx: HogQLParser.ColumnExprAliasContext): raise NotImplementedError(f"Must specify an alias.") expr = self.visit(ctx.columnExpr()) - if alias in RESERVED_KEYWORDS or alias in KEYWORDS: + if alias in RESERVED_KEYWORDS: raise ValueError(f"Alias '{alias}' is a reserved keyword.") return ast.Alias(expr=expr, alias=alias) @@ -542,7 +542,7 @@ def visitTableExprSubquery(self, ctx: HogQLParser.TableExprSubqueryContext): def visitTableExprAlias(self, ctx: HogQLParser.TableExprAliasContext): alias = self.visit(ctx.alias() or ctx.identifier()) - if alias in RESERVED_KEYWORDS or alias in KEYWORDS: + if alias in RESERVED_KEYWORDS: raise ValueError(f"Alias '{alias}' is a reserved keyword.") return ast.JoinExpr(table=self.visit(ctx.tableExpr()), alias=alias) From 5cea7bdcc95c137311c4ec9bb32f10650f87743a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 13:02:52 +0100 Subject: [PATCH 034/142] resolver cleanup --- posthog/hogql/database.py | 6 +++++ posthog/hogql/resolver.py | 51 +++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index c9fa18fb84a30..314b8621a00b9 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -115,5 +115,11 @@ class Config: person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() + def get_table(self, table_name: str): + return getattr(self, table_name) + + def has_table(self, table_name: str): + return hasattr(self, table_name) + database = Database() diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 8c76bf9c58f24..b86bfc5c286c2 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -27,26 +27,26 @@ def visit_select_query(self, node): if node.symbol is not None: return + # This symbol keeps track of all joined tables and other field aliases that are in scope. node.symbol = ast.SelectQuerySymbol() - # Each SELECT query creates a new scope. Store all of them in a list for variable access. + # Each SELECT query is a new scope in field name resolution. self.scopes.append(node.symbol) - # Visit all the FROM and JOIN clauses (JoinExpr nodes), and register the tables into the scope. + # Visit all the FROM and JOIN clauses, and register the tables into the scope. See visit_join_expr below. if node.select_from: self.visit(node.select_from) - # Visit all the SELECT columns. - # Then mark them for export in "columns". This means they will be available outside of this query via: + # Visit all the SELECT 1,2,3 columns. Mark each for export in "columns" to make this work: # SELECT e.event, e.timestamp from (SELECT event, timestamp FROM events) AS e for expr in node.select or []: self.visit(expr) if isinstance(expr.symbol, ast.FieldAliasSymbol): node.symbol.columns[expr.symbol.name] = expr.symbol - elif isinstance(expr, ast.Alias): - node.symbol.columns[expr.alias] = expr.symbol elif isinstance(expr.symbol, ast.FieldSymbol): node.symbol.columns[expr.symbol.name] = expr.symbol + elif isinstance(expr, ast.Alias): + node.symbol.columns[expr.alias] = expr.symbol if node.where: self.visit(node.where) @@ -80,15 +80,14 @@ def visit_join_expr(self, node): table_name = node.table.chain[0] table_alias = node.alias or table_name if table_alias in scope.tables: - raise ResolverException(f'Already have a joined table called "{table_alias}", can\'t redefine.') + raise ResolverException(f'Already have joined a table called "{table_alias}". Can\'t redefine.') - if table_name in database.__fields__: - table = database.__fields__[table_name].default - node.table.symbol = ast.TableSymbol(table=table) - if table_alias != table_name: - node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.table.symbol) - else: + if database.has_table(table_name): + node.table.symbol = ast.TableSymbol(table=database.get_table(table_name)) + if table_alias == table_name: node.symbol = node.table.symbol + else: + node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.table.symbol) scope.tables[table_alias] = node.symbol else: raise ResolverException(f'Unknown table "{table_name}".') @@ -97,7 +96,7 @@ def visit_join_expr(self, node): node.table.symbol = self.visit(node.table) if node.alias is not None: if node.alias in scope.tables: - raise ResolverException(f'Already have a joined table called "{node.alias}", can\'t redefine.') + raise ResolverException(f'Already have joined a table called "{node.alias}". Can\'t redefine.') node.symbol = ast.SelectQueryAliasSymbol(name=node.alias, symbol=node.table.symbol) scope.tables[node.alias] = node.symbol else: @@ -133,9 +132,12 @@ def visit_call(self, node: ast.Call): """Visit function calls.""" if node.symbol is not None: return + arg_symbols: List[ast.Symbol] = [] for arg in node.args: self.visit(arg) - node.symbol = ast.CallSymbol(name=node.name, args=[arg.symbol for arg in node.args]) + if arg.symbol is not None: + arg_symbols.append(arg.symbol) + node.symbol = ast.CallSymbol(name=node.name, args=arg_symbols) def visit_field(self, node): """Visit a field such as ast.Field(chain=["e", "properties", "$browser"])""" @@ -144,18 +146,19 @@ def visit_field(self, node): if len(node.chain) == 0: raise Exception("Invalid field access with empty chain") - # ClickHouse does not support subqueries accessing "x.event" like this: + # Only look for fields in the last SELECT scope. + scope = self.scopes[-1] + + # ClickHouse does not support subqueries accessing "x.event". This is forbidden: # - "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", # But this is supported: # - "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t JOIN events e ON (e.event = t.event)", - # - # Thus we only look into scopes[-1] to see aliases in the current scope, and don't loop over all the scopes. + # Thus we don't have to recursively look into all the past scopes to find a match. - scope = self.scopes[-1] symbol: Optional[ast.Symbol] = None name = node.chain[0] - # Only look for matching tables if field contains at least two parts. + # If the field contains at least two parts, the first might be a table. if len(node.chain) > 1 and name in scope.tables: symbol = scope.tables[name] if not symbol: @@ -187,10 +190,10 @@ def lookup_field_by_name(scope: ast.SelectQuerySymbol, name: str) -> Optional[as else: named_tables = [table for table in scope.tables.values() if table.has_child(name)] anonymous_tables = [table for table in scope.anonymous_tables if table.has_child(name)] - tables = named_tables + anonymous_tables + tables_with_field = named_tables + anonymous_tables - if len(tables) > 1: + if len(tables_with_field) > 1: raise ResolverException(f"Ambiguous query. Found multiple sources for field: {name}") - elif len(tables) == 1: - return tables[0].get_child(name) + elif len(tables_with_field) == 1: + return tables_with_field[0].get_child(name) return None From 9e995fb5dc831a06dd2594fc9bce9417f7d9f9b7 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 13:19:40 +0100 Subject: [PATCH 035/142] bit of printer cleanup --- posthog/hogql/printer.py | 83 ++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 32 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 47797080192db..cbce47c79caec 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1,4 +1,5 @@ -from typing import List, Literal, Optional, Tuple, Union +from dataclasses import dataclass +from typing import List, Literal, Optional, Union from ee.clickhouse.materialized_columns.columns import get_materialized_columns from posthog.hogql import ast @@ -9,7 +10,9 @@ from posthog.hogql.visitor import Visitor -def guard_where_team_id(table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbol], context: HogQLContext) -> ast.Expr: +def team_id_guard_for_table( + table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbol], context: HogQLContext +) -> ast.Expr: """Add a mandatory "and(team_id, ...)" filter around the expression.""" if not context.select_team_id: raise ValueError("context.select_team_id not found") @@ -22,9 +25,15 @@ def guard_where_team_id(table_symbol: Union[ast.TableSymbol, ast.TableAliasSymbo def print_ast( - node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: List[ast.AST] = [] + node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: Optional[List[ast.AST]] = None ) -> str: - return Printer(context=context, dialect=dialect, stack=stack).visit(node) + return Printer(context=context, dialect=dialect, stack=stack or []).visit(node) + + +@dataclass +class JoinExprResponse: + printed_sql: str + where: Optional[ast.Expr] = None class Printer(Visitor): @@ -33,9 +42,11 @@ def __init__( ): self.context = context self.dialect = dialect + # Keep track of all traversed nodes. self.stack: List[ast.AST] = stack or [] def _last_select(self) -> Optional[ast.SelectQuery]: + """Find the last SELECT query in the stack.""" for node in reversed(self.stack): if isinstance(node, ast.SelectQuery): return node @@ -49,9 +60,9 @@ def visit(self, node: ast.AST): def visit_select_query(self, node: ast.SelectQuery): if self.dialect == "clickhouse" and not self.context.select_team_id: - raise ValueError("Full SELECT queries are disabled if select_team_id is not set") + raise ValueError("Full SELECT queries are disabled if context.select_team_id is not set") - # we will add extra clauses onto this + # We will add extra clauses onto this from the joined tables where = node.where select_from = [] @@ -60,16 +71,22 @@ def visit_select_query(self, node: ast.SelectQuery): if next_join.symbol is None: raise ValueError("Printing queries with a FROM clause is not permitted before symbol resolution") - (select_sql, extra_where) = self.visit_join_expr(next_join) - select_from.append(select_sql) + visited_join = self.visit_join_expr(next_join) + select_from.append(visited_join.printed_sql) - if extra_where is not None: + # This is an expression we must add to the SELECT's WHERE clause to limit results. + extra_where = visited_join.where + if extra_where is None: + pass + elif isinstance(extra_where, ast.Expr): if where is None: where = extra_where elif isinstance(where, ast.And): where = ast.And(exprs=[extra_where] + where.exprs) else: where = ast.And(exprs=[extra_where, where]) + else: + raise ValueError(f"Invalid where of type {type(extra_where).__name__} returned by join_expr") next_join = next_join.next_join @@ -80,16 +97,6 @@ def visit_select_query(self, node: ast.SelectQuery): group_by = [self.visit(column) for column in node.group_by] if node.group_by else None order_by = [self.visit(column) for column in node.order_by] if node.order_by else None - limit = node.limit - if self.context.limit_top_select: - if limit is not None: - if isinstance(limit, ast.Constant) and isinstance(limit.value, int): - limit.value = min(limit.value, MAX_SELECT_RETURNED_ROWS) - else: - limit = ast.Call(name="min2", args=[ast.Constant(value=MAX_SELECT_RETURNED_ROWS), limit]) - elif len(self.stack) == 1: - limit = ast.Constant(value=MAX_SELECT_RETURNED_ROWS) - clauses = [ f"SELECT {'DISTINCT ' if node.distinct else ''}{', '.join(columns)}", f"FROM {' '.join(select_from)}" if len(select_from) > 0 else None, @@ -99,6 +106,17 @@ def visit_select_query(self, node: ast.SelectQuery): "PREWHERE " + prewhere if prewhere else None, f"ORDER BY {', '.join(order_by)}" if order_by and len(order_by) > 0 else None, ] + + limit = node.limit + if self.context.limit_top_select and len(self.stack) == 1: + if limit is not None: + if isinstance(limit, ast.Constant) and isinstance(limit.value, int): + limit.value = min(limit.value, MAX_SELECT_RETURNED_ROWS) + else: + limit = ast.Call(name="min2", args=[ast.Constant(value=MAX_SELECT_RETURNED_ROWS), limit]) + else: + limit = ast.Constant(value=MAX_SELECT_RETURNED_ROWS) + if limit is not None: clauses.append(f"LIMIT {self.visit(limit)}") if node.offset is not None: @@ -113,9 +131,9 @@ def visit_select_query(self, node: ast.SelectQuery): response = f"({response})" return response - def visit_join_expr(self, node: ast.JoinExpr) -> Tuple[str, Optional[ast.Expr]]: + def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: # return constraints we must place on the select query - extra_where = None + extra_where: Optional[ast.Expr] = None select_from = [] if node.join_type is not None: @@ -132,13 +150,13 @@ def visit_join_expr(self, node: ast.JoinExpr) -> Tuple[str, Optional[ast.Expr]]: select_from.append(f"AS {print_hogql_identifier(node.alias)}") if self.dialect == "clickhouse": - extra_where = guard_where_team_id(node.symbol, self.context) + extra_where = team_id_guard_for_table(node.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) if self.dialect == "clickhouse": - extra_where = guard_where_team_id(node.symbol, self.context) + extra_where = team_id_guard_for_table(node.symbol, self.context) elif isinstance(node.symbol, ast.SelectQuerySymbol): select_from.append(self.visit(node.table)) @@ -155,7 +173,7 @@ def visit_join_expr(self, node: ast.JoinExpr) -> Tuple[str, Optional[ast.Expr]]: if node.constraint is not None: select_from.append(f"ON {self.visit(node.constraint)}") - return (" ".join(select_from), extra_where) + return JoinExprResponse(printed_sql=" ".join(select_from), where=extra_where) def visit_binary_operation(self, node: ast.BinaryOperation): if node.op == ast.BinaryOperationType.Add: @@ -244,16 +262,17 @@ def visit_field(self, node: ast.Field): raise ValueError(f"Field {original_field} has no symbol") if self.dialect == "hogql": - # When printing HogQL, we print the properties out as a chain instead of converting them to Clickhouse SQL + # When printing HogQL, we print the properties out as a chain as they are. return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) - elif node.chain == ["*"]: - raise ValueError("Selecting * not implemented") + + if node.chain == ["*"]: # query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" # return self.visit(parse_expr(query)) + raise ValueError("Selecting * not yet implemented") elif node.chain == ["person"]: - raise ValueError("Selecting person not implemented") # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" # return self.visit(parse_expr(query)) + raise ValueError("Selecting person not yet implemented") elif node.symbol is not None: select_query = self._last_select() select: Optional[ast.SelectQuerySymbol] = select_query.symbol if select_query else None @@ -261,7 +280,7 @@ def visit_field(self, node: ast.Field): raise ValueError(f"Can't find SelectQuerySymbol for field: {original_field}") return SymbolPrinter(select=select, context=self.context).visit(node.symbol) else: - raise ValueError(f"Unknown Symbol, can not print {type(node.symbol)}") + raise ValueError(f"Unknown Symbol, can not print {type(node.symbol).__name__}") def visit_call(self, node: ast.Call): if node.name in HOGQL_AGGREGATIONS: @@ -337,8 +356,8 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): else: field_sql = printed_field + # TODO: refactor this property access logging, also add person properties if printed_field != "properties": - # TODO: refactor this property access logging self.context.field_access_logs.append( HogQLFieldAccess( [symbol.name], @@ -355,7 +374,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): self.context.values[key] = symbol.name table = symbol.field.table - if isinstance(table, ast.TableAliasSymbol): + while isinstance(table, ast.TableAliasSymbol): table = table.table # TODO: cache this @@ -382,7 +401,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return print_hogql_identifier(symbol.name) - def visit_column_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): + def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return print_hogql_identifier(symbol.name) def visit_unknown(self, symbol: ast.AST): From 166e7fe30356da2d7be9e7e5457d838b4be0464f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 15:28:40 +0100 Subject: [PATCH 036/142] placeholders in query --- posthog/hogql/query.py | 11 +++++++++-- posthog/hogql/test/test_query.py | 21 +++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index f47e48707a987..7938da035b6fd 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -1,4 +1,4 @@ -from typing import List, Optional, Union +from typing import Dict, List, Optional, Union from pydantic import BaseModel, Extra @@ -6,6 +6,7 @@ from posthog.hogql import ast from posthog.hogql.hogql import HogQLContext from posthog.hogql.parser import parse_select +from posthog.hogql.placeholders import assert_no_placeholders, replace_placeholders from posthog.hogql.printer import print_ast from posthog.hogql.resolver import resolve_symbols from posthog.models import Team @@ -28,13 +29,19 @@ def execute_hogql_query( query: Union[str, ast.SelectQuery], team: Team, query_type: str = "hogql_query", + placeholders: Optional[Dict[str, ast.Expr]] = None, workload: Workload = Workload.ONLINE, ) -> HogQLQueryResponse: if isinstance(query, ast.SelectQuery): select_query = query query = None else: - select_query = parse_select(str(query), no_placeholders=True) + select_query = parse_select(str(query)) + + if placeholders: + select_query = replace_placeholders(select_query, placeholders) + else: + assert_no_placeholders(select_query) if select_query.limit is None: select_query.limit = ast.Constant(value=1000) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index c1cefa49361f0..25c9256bad1fd 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -1,5 +1,6 @@ from freezegun import freeze_time +from posthog.hogql import ast from posthog.hogql.query import execute_hogql_query from posthog.models.utils import UUIDT from posthog.test.base import APIBaseTest, ClickhouseTestMixin, _create_event, _create_person, flush_persons_and_events @@ -30,8 +31,9 @@ def test_query(self): random_uuid = self._create_random_events() response = execute_hogql_query( - f"select count(), event from events where properties.random_uuid = '{random_uuid}' group by event", - self.team, + "select count(), event from events where properties.random_uuid = {random_uuid} group by event", + placeholders={"random_uuid": ast.Constant(value=random_uuid)}, + team=self.team, ) self.assertEqual( response.clickhouse, @@ -44,8 +46,9 @@ def test_query(self): self.assertEqual(response.results, [(2, "random event")]) response = execute_hogql_query( - f"select count, event from (select count() as count, event from events where properties.random_uuid = '{random_uuid}' group by event) group by count, event", - self.team, + "select count, event from (select count() as count, event from events where properties.random_uuid = {random_uuid} group by event) group by count, event", + placeholders={"random_uuid": ast.Constant(value=random_uuid)}, + team=self.team, ) self.assertEqual( response.clickhouse, @@ -58,8 +61,9 @@ def test_query(self): self.assertEqual(response.results, [(2, "random event")]) response = execute_hogql_query( - f"select count, event from (select count() as count, event from events where properties.random_uuid = '{random_uuid}' group by event) as c group by count, event", - self.team, + "select count, event from (select count() as count, event from events where properties.random_uuid = {random_uuid} group by event) as c group by count, event", + placeholders={"random_uuid": ast.Constant(value=random_uuid)}, + team=self.team, ) self.assertEqual( response.clickhouse, @@ -72,8 +76,9 @@ def test_query(self): self.assertEqual(response.results, [(2, "random event")]) response = execute_hogql_query( - f"select distinct properties.email from persons where properties.random_uuid = '{random_uuid}'", - self.team, + "select distinct properties.email from persons where properties.random_uuid = {random_uuid}", + placeholders={"random_uuid": ast.Constant(value=random_uuid)}, + team=self.team, ) self.assertEqual( response.clickhouse, From 2287c07033b4846e8b878fdf0425b3c14a0e27ef Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 19:10:36 +0100 Subject: [PATCH 037/142] changes --- posthog/hogql/ast.py | 28 +++--- posthog/hogql/database.py | 136 +++++++++++++++++------------ posthog/hogql/printer.py | 7 +- posthog/hogql/test/test_printer.py | 2 +- 4 files changed, 104 insertions(+), 69 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index ee4854ecbe202..7c3aedb466185 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import StringJSONValue, Table +from posthog.hogql.database import ComplexField, DatabaseField, StringJSONDatabaseField, Table # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -29,7 +29,7 @@ def accept(self, visitor): class Symbol(AST): def get_child(self, name: str) -> "Symbol": - raise NotImplementedError() + raise NotImplementedError("Symbol.get_child not overridden") def has_child(self, name: str) -> bool: return self.get_child(name) is not None @@ -50,7 +50,7 @@ class TableSymbol(Symbol): table: Table def has_child(self, name: str) -> bool: - return name in self.table.__fields__ + return self.table.has_field(name) def get_child(self, name: str) -> Symbol: if self.has_child(name): @@ -114,6 +114,10 @@ class CallSymbol(Symbol): args: List[Symbol] +class ConstantSymbol(Symbol): + value: Any + + class FieldSymbol(Symbol): name: str table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] @@ -126,20 +130,22 @@ def get_child(self, name: str) -> Symbol: if isinstance(table_symbol, TableSymbol): db_table = table_symbol.table if isinstance(db_table, Table): - if self.name in db_table.__fields__ and isinstance( - db_table.__fields__[self.name].default, StringJSONValue + if db_table.has_field(self.name) and ( + isinstance(db_table.get_field(self.name), StringJSONDatabaseField) + or isinstance(db_table.get_field(self.name), ComplexField) ): - return PropertySymbol(name=name, field=self) - raise ValueError(f"Can not access property {name} on field {self.name}.") + return PropertySymbol(name=name, property=db_table.get_field(self.name), parent=self) - -class ConstantSymbol(Symbol): - value: Any + raise ValueError(f'Can not access property "{name}" on field "{self.name}".') class PropertySymbol(Symbol): name: str - field: FieldSymbol + property: Union[DatabaseField, ComplexField] + parent: Union[FieldSymbol, "PropertySymbol"] + + +PropertySymbol.update_forward_refs(PropertySymbol=PropertySymbol) class Expr(AST): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 314b8621a00b9..8fe9b25754609 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,105 +1,129 @@ +from typing import Union + from pydantic import BaseModel, Extra -class Field(BaseModel): +class DatabaseField(BaseModel): + """Base class for a field in a database table.""" + class Config: extra = Extra.forbid + name: str + array: bool = False -class IntegerValue(Field): - pass +class ComplexField(BaseModel): + """Base class for a complex field with custom properties.""" + + class Config: + extra = Extra.forbid + + def has_child(self, name: str) -> bool: + return hasattr(self, name) -class StringValue(Field): + def get_child(self, name: str) -> DatabaseField: + return getattr(self, name) + + +class IntegerDatabaseField(DatabaseField): pass -class StringJSONValue(Field): +class StringDatabaseField(DatabaseField): pass -class DateTimeValue(Field): +class StringJSONDatabaseField(DatabaseField): pass -class BooleanValue(Field): +class DateTimeDatabaseField(DatabaseField): pass -class ArrayValue(Field): - field: Field +class BooleanDatabaseField(DatabaseField): + pass class Table(BaseModel): class Config: extra = Extra.forbid + def has_field(self, name: str) -> bool: + return hasattr(self, name) + + def get_field(self, name: str) -> Union[DatabaseField, ComplexField]: + if self.has_field(name): + return getattr(self, name) + raise ValueError(f'Field "{name}" not found on table {self.__class__.__name__}') + def clickhouse_table(self): - raise NotImplementedError() + raise NotImplementedError("Table.clickhouse_table not overridden") class PersonsTable(Table): - id: StringValue = StringValue() - created_at: DateTimeValue = DateTimeValue() - team_id: IntegerValue = IntegerValue() - properties: StringJSONValue = StringJSONValue() - is_identified: BooleanValue = BooleanValue() - is_deleted: BooleanValue = BooleanValue() - version: IntegerValue = IntegerValue() + 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" class PersonDistinctIdTable(Table): - team_id: IntegerValue = IntegerValue() - distinct_id: StringValue = StringValue() - person_id: StringValue = StringValue() - is_deleted: BooleanValue = BooleanValue() - version: IntegerValue = IntegerValue() + 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" -class PersonFieldsOnEvents(Table): - id: StringValue = StringValue() - created_at: DateTimeValue = DateTimeValue() - properties: StringJSONValue = StringJSONValue() +class EventsPersonComplexField(ComplexField): + id: StringDatabaseField = StringDatabaseField(name="person_id") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="person_created_at") + properties: StringJSONDatabaseField = StringJSONDatabaseField(name="person_properties") class EventsTable(Table): - uuid: StringValue = StringValue() - event: StringValue = StringValue() - timestamp: DateTimeValue = DateTimeValue() - properties: StringJSONValue = StringJSONValue() - elements_chain: StringValue = StringValue() - created_at: DateTimeValue = DateTimeValue() - distinct_id: StringValue = StringValue() - team_id: IntegerValue = IntegerValue() - person: PersonFieldsOnEvents = PersonFieldsOnEvents() + uuid: StringDatabaseField = StringDatabaseField(name="uuid") + event: StringDatabaseField = StringDatabaseField(name="event") + timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamp") + properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") + elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") + distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + person: EventsPersonComplexField = EventsPersonComplexField() def clickhouse_table(self): return "events" class SessionRecordingEvents(Table): - uuid: StringValue = StringValue() - timestamp: DateTimeValue = DateTimeValue() - team_id: IntegerValue = IntegerValue() - distinct_id: StringValue = StringValue() - session_id: StringValue = StringValue() - window_id: StringValue = StringValue() - snapshot_data: StringValue = StringValue() - created_at: DateTimeValue = DateTimeValue() - has_full_snapshot: BooleanValue = BooleanValue() - events_summary: ArrayValue = ArrayValue(field=BooleanValue()) - click_count: IntegerValue = IntegerValue() - keypress_count: IntegerValue = IntegerValue() - timestamps_summary: ArrayValue = ArrayValue(field=DateTimeValue()) - first_event_timestamp: DateTimeValue = DateTimeValue() - last_event_timestamp: DateTimeValue = DateTimeValue() - urls: ArrayValue = ArrayValue(field=StringValue()) + uuid: StringDatabaseField = StringDatabaseField(name="uuid") + timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamp") + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") + session_id: StringDatabaseField = StringDatabaseField(name="session_id") + window_id: StringDatabaseField = StringDatabaseField(name="window_id") + snapshot_data: StringDatabaseField = StringDatabaseField(name="snapshot_data") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") + has_full_snapshot: BooleanDatabaseField = BooleanDatabaseField(name="has_full_snapshot") + events_summary: BooleanDatabaseField = BooleanDatabaseField(name="events_summary", array=True) + click_count: IntegerDatabaseField = IntegerDatabaseField(name="click_count") + keypress_count: IntegerDatabaseField = IntegerDatabaseField(name="keypress_count") + timestamps_summary: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamps_summary", array=True) + first_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="first_event_timestamp") + last_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="last_event_timestamp") + urls: StringDatabaseField = StringDatabaseField(name="urls", array=True) def clickhouse_table(self): return "session_recording_events" @@ -115,11 +139,13 @@ class Config: person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() - def get_table(self, table_name: str): - return getattr(self, table_name) - - def has_table(self, table_name: str): + def has_table(self, table_name: str) -> bool: return hasattr(self, table_name) + def get_table(self, table_name: str) -> Table: + if self.has_table(table_name): + return getattr(self, table_name) + raise ValueError(f'Table "{table_name}" not found in database') + database = Database() diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index cbce47c79caec..4511ce66775ed 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -370,10 +370,13 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): return field_sql def visit_property_symbol(self, symbol: ast.PropertySymbol): + if isinstance(symbol.parent, ast.PropertySymbol): + return self.visit_property_symbol(symbol.parent) + key = f"hogql_val_{len(self.context.values)}" self.context.values[key] = symbol.name - table = symbol.field.table + table = symbol.parent.table while isinstance(table, ast.TableAliasSymbol): table = table.table @@ -384,7 +387,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): if materialized_column: property_sql = print_hogql_identifier(materialized_column) else: - field_sql = self.visit(symbol.field) + field_sql = self.visit(symbol.parent) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") self.context.field_access_logs.append( diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index bf1a055ee474b..04136ff5738ec 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -208,7 +208,7 @@ def test_expr_parse_errors(self): self._assert_expr_error( "avg(avg(properties.bla))", "Aggregation 'avg' cannot be nested inside another aggregation 'avg'." ) - self._assert_expr_error("person.chipotle", "Unknown person field 'chipotle'") + self._assert_expr_error("person.chipotle", 'Can not access property "chipotle" on compelx field "person".') def test_expr_syntax_errors(self): self._assert_expr_error("(", "line 1, column 1: no viable alternative at input '('") From 92983fb36a68637efde78a4df98e7ebd6a2dce8d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 21:57:25 +0100 Subject: [PATCH 038/142] person properties fake table --- posthog/hogql/ast.py | 32 ++++----- posthog/hogql/database.py | 25 ++----- posthog/hogql/printer.py | 106 +++++++++++++++++++---------- posthog/hogql/resolver.py | 8 +-- posthog/hogql/test/test_printer.py | 1 - 5 files changed, 98 insertions(+), 74 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 7c3aedb466185..eb18afdda34a5 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import ComplexField, DatabaseField, StringJSONDatabaseField, Table +from posthog.hogql.database import DatabaseField, StringJSONDatabaseField, Table # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -122,30 +122,30 @@ class FieldSymbol(Symbol): name: str table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] - def get_child(self, name: str) -> Symbol: + def resolve_database_field(self) -> Optional[Union[DatabaseField, Table]]: table_symbol = self.table while isinstance(table_symbol, TableAliasSymbol): table_symbol = table_symbol.table - if isinstance(table_symbol, TableSymbol): - db_table = table_symbol.table - if isinstance(db_table, Table): - if db_table.has_field(self.name) and ( - isinstance(db_table.get_field(self.name), StringJSONDatabaseField) - or isinstance(db_table.get_field(self.name), ComplexField) - ): - return PropertySymbol(name=name, property=db_table.get_field(self.name), parent=self) + return table_symbol.table.get_field(self.name) + return None - raise ValueError(f'Can not access property "{name}" on field "{self.name}".') + def get_child(self, name: str) -> Symbol: + database_field = self.resolve_database_field() + if database_field is None: + raise ValueError(f'Can not access property "{name}" on field "{self.name}".') + if isinstance(database_field, Table): + return FieldSymbol(name=name, table=TableSymbol(table=database_field)) + if isinstance(database_field, StringJSONDatabaseField): + return PropertySymbol(name=name, parent=self) + raise ValueError( + f'Can not access property "{name}" on field "{self.name}" of type: {type(database_field).__name__}' + ) class PropertySymbol(Symbol): name: str - property: Union[DatabaseField, ComplexField] - parent: Union[FieldSymbol, "PropertySymbol"] - - -PropertySymbol.update_forward_refs(PropertySymbol=PropertySymbol) + parent: FieldSymbol class Expr(AST): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 8fe9b25754609..dc9c8bf87d3ed 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,5 +1,3 @@ -from typing import Union - from pydantic import BaseModel, Extra @@ -13,19 +11,6 @@ class Config: array: bool = False -class ComplexField(BaseModel): - """Base class for a complex field with custom properties.""" - - class Config: - extra = Extra.forbid - - def has_child(self, name: str) -> bool: - return hasattr(self, name) - - def get_child(self, name: str) -> DatabaseField: - return getattr(self, name) - - class IntegerDatabaseField(DatabaseField): pass @@ -53,7 +38,7 @@ class Config: def has_field(self, name: str) -> bool: return hasattr(self, name) - def get_field(self, name: str) -> Union[DatabaseField, ComplexField]: + def get_field(self, name: str) -> DatabaseField: if self.has_field(name): return getattr(self, name) raise ValueError(f'Field "{name}" not found on table {self.__class__.__name__}') @@ -86,11 +71,15 @@ def clickhouse_table(self): return "person_distinct_id2" -class EventsPersonComplexField(ComplexField): +class EventsPersonSubTable(Table): id: StringDatabaseField = StringDatabaseField(name="person_id") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="person_created_at") properties: StringJSONDatabaseField = StringJSONDatabaseField(name="person_properties") + def clickhouse_table(self): + # This is a bit of a hack to make sure person.properties.x works + return "events" + class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") @@ -101,7 +90,7 @@ class EventsTable(Table): created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - person: EventsPersonComplexField = EventsPersonComplexField() + person: EventsPersonSubTable = EventsPersonSubTable() def clickhouse_table(self): return "events" diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4511ce66775ed..c141298f68efa 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1,10 +1,11 @@ from dataclasses import dataclass -from typing import List, Literal, Optional, Union +from typing import List, Literal, Optional, Union, cast from ee.clickhouse.materialized_columns.columns import get_materialized_columns from posthog.hogql import ast from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess +from posthog.hogql.database import database from posthog.hogql.print_string import print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name from posthog.hogql.visitor import Visitor @@ -339,65 +340,100 @@ def visit_table_alias_symbol(self, symbol: ast.TableAliasSymbol): return print_hogql_identifier(symbol.name) def visit_field_symbol(self, symbol: ast.FieldSymbol): - printed_field = print_hogql_identifier(symbol.name) - try: symbol_with_name_in_scope = lookup_field_by_name(self.select, symbol.name) except ResolverException: symbol_with_name_in_scope = None - if ( - symbol_with_name_in_scope != symbol - or isinstance(symbol.table, ast.TableAliasSymbol) - or isinstance(symbol.table, ast.SelectQueryAliasSymbol) - ): - table_prefix = self.visit(symbol.table) - field_sql = f"{table_prefix}.{printed_field}" - else: - field_sql = printed_field - - # TODO: refactor this property access logging, also add person properties - if printed_field != "properties": - self.context.field_access_logs.append( - HogQLFieldAccess( - [symbol.name], - "event", - symbol.name, - field_sql, + if isinstance(symbol.table, ast.TableSymbol) or isinstance(symbol.table, ast.TableAliasSymbol): + resolved_field = symbol.resolve_database_field() + if resolved_field is None: + raise ValueError(f'Can\'t resolve field "{symbol.name}" on table.') + + field_sql = print_hogql_identifier(resolved_field.name) + if ( + resolved_field.name != symbol.name + or isinstance(symbol.table, ast.TableAliasSymbol) + or symbol_with_name_in_scope != symbol + ): + field_sql = f"{self.visit(symbol.table)}.{field_sql}" + + # TODO: refactor this lefacy logging + if symbol.name != "properties": + real_table = symbol.table + while isinstance(real_table, ast.TableAliasSymbol): + real_table = real_table.table + + self.context.field_access_logs.append( + HogQLFieldAccess( + [symbol.name], + "event" if real_table.table == database.events else "person", + symbol.name, + field_sql, + ) ) - ) + + elif isinstance(symbol.table, ast.SelectQuerySymbol) or isinstance(symbol.table, ast.SelectQueryAliasSymbol): + field_sql = print_hogql_identifier(symbol.name) + if isinstance(symbol.table, ast.SelectQueryAliasSymbol) or symbol_with_name_in_scope != symbol: + field_sql = f"{self.visit(symbol.table)}.{field_sql}" + + else: + raise ValueError(f"Unknown FieldSymbol table type: {type(symbol.table).__name__}") return field_sql def visit_property_symbol(self, symbol: ast.PropertySymbol): - if isinstance(symbol.parent, ast.PropertySymbol): - return self.visit_property_symbol(symbol.parent) + field_symbol = symbol.parent key = f"hogql_val_{len(self.context.values)}" self.context.values[key] = symbol.name - table = symbol.parent.table + table = field_symbol.table while isinstance(table, ast.TableAliasSymbol): table = table.table + if not isinstance(table, ast.TableSymbol): + raise ValueError(f"Unknown PropertySymbol table type: {type(table).__name__}") + + table_name = table.table.clickhouse_table() + + field = field_symbol.resolve_database_field() + if field is None: + raise ValueError(f"Can't resolve field {field_symbol.name} on table {table_name}") + + field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) + # TODO: cache this - materialized_columns = get_materialized_columns(table.table.clickhouse_table()) - materialized_column = materialized_columns.get((symbol.name, "properties"), None) + materialized_columns = get_materialized_columns(table_name) + materialized_column = materialized_columns.get((symbol.name, field_name), None) if materialized_column: property_sql = print_hogql_identifier(materialized_column) else: - field_sql = self.visit(symbol.parent) + field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") - self.context.field_access_logs.append( - HogQLFieldAccess( - ["properties", symbol.name], - "event.properties", - symbol.name, - property_sql, + if field_name == "properties": + # TODO: refactor this lefacy logging + self.context.field_access_logs.append( + HogQLFieldAccess( + ["properties", symbol.name], + "event.properties", + symbol.name, + property_sql, + ) + ) + elif field_name == "person_properties": + # TODO: refactor this lefacy logging + self.context.field_access_logs.append( + HogQLFieldAccess( + ["person", "properties", symbol.name], + "person.properties", + symbol.name, + property_sql, + ) ) - ) return property_sql diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index b86bfc5c286c2..b9ecb1c00bcdb 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -167,14 +167,14 @@ def visit_field(self, node): raise ResolverException(f"Unable to resolve field: {name}") # Recursively resolve the rest of the chain until we can point to the deepest node. + loop_symbol = symbol for child_name in node.chain[1:]: - symbol = symbol.get_child(child_name) - if symbol is None: + loop_symbol = loop_symbol.get_child(child_name) + if loop_symbol is None: raise ResolverException( f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {child_name} on {name}" ) - - node.symbol = symbol + node.symbol = loop_symbol def visit_constant(self, node): """Visit a constant""" diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 04136ff5738ec..4d11eaf5dcc94 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -177,7 +177,6 @@ def test_materialized_fields_and_properties(self): materialize("events", "$browser%%%#@!@") self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "mat_$browser_______") - # TODO: get person properties working materialize("events", "$initial_waffle", table_column="person_properties") self.assertEqual(self._expr("person.properties['$initial_waffle']"), "mat_pp_$initial_waffle") From 66b47ab6bcc97b191dc07f1acacd5af4ec1c645d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 14 Feb 2023 22:14:33 +0100 Subject: [PATCH 039/142] pass two more tests --- posthog/hogql/test/test_printer.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 4d11eaf5dcc94..ef6bf47e77a7c 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -74,7 +74,7 @@ def test_fields_and_properties(self): context = HogQLContext() self.assertEqual( self._expr("person.properties.bla", context), - "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(events.person_properties, %(hogql_val_0)s), '^\"|\"$', '')", ) self.assertEqual( context.field_access_logs, @@ -83,7 +83,7 @@ def test_fields_and_properties(self): ["person", "properties", "bla"], "person.properties", "bla", - "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(events.person_properties, %(hogql_val_0)s), '^\"|\"$', '')", ) ], ) @@ -109,14 +109,17 @@ def test_fields_and_properties(self): ) context = HogQLContext() - self.assertEqual(self._expr("person_id", context), "person_id") - self.assertEqual(context.field_access_logs, [HogQLFieldAccess(["person_id"], "person", "id", "person_id")]) + self.assertEqual(self._expr("person.id", context), "events.person_id") + self.assertEqual( + context.field_access_logs, + [HogQLFieldAccess(["id"], "person", "id", "events.person_id")], + ) context = HogQLContext() - self.assertEqual(self._expr("person.created_at", context), "person_created_at") + self.assertEqual(self._expr("person.created_at", context), "events.person_created_at") self.assertEqual( context.field_access_logs, - [HogQLFieldAccess(["person", "created_at"], "person", "created_at", "person_created_at")], + [HogQLFieldAccess(["created_at"], "person", "created_at", "events.person_created_at")], ) def test_hogql_properties(self): @@ -207,7 +210,7 @@ def test_expr_parse_errors(self): self._assert_expr_error( "avg(avg(properties.bla))", "Aggregation 'avg' cannot be nested inside another aggregation 'avg'." ) - self._assert_expr_error("person.chipotle", 'Can not access property "chipotle" on compelx field "person".') + self._assert_expr_error("person.chipotle", 'Field "chipotle" not found on table EventsPersonSubTable') def test_expr_syntax_errors(self): self._assert_expr_error("(", "line 1, column 1: no viable alternative at input '('") From fa22dddee717b030e579d41c39e1ac6153977d92 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 09:09:17 +0100 Subject: [PATCH 040/142] legacy person properties --- posthog/hogql/printer.py | 40 ++++++++++++++++++++---------- posthog/hogql/test/test_printer.py | 4 +-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index c141298f68efa..4511cd4b9b601 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from typing import List, Literal, Optional, Union, cast -from ee.clickhouse.materialized_columns.columns import get_materialized_columns +from ee.clickhouse.materialized_columns.columns import TablesWithMaterializedColumns, get_materialized_columns from posthog.hogql import ast from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess @@ -9,6 +9,7 @@ from posthog.hogql.print_string import print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name from posthog.hogql.visitor import Visitor +from posthog.models.property import PropertyName, TableColumn def team_id_guard_for_table( @@ -333,6 +334,13 @@ def __init__(self, select: ast.SelectQuerySymbol, context: HogQLContext): self.select = select self.context = context + def _get_materialized_column( + self, table_name: TablesWithMaterializedColumns, property_name: PropertyName, field_name: TableColumn + ) -> Optional[str]: + materialized_columns = get_materialized_columns(table_name) + materialized_column = materialized_columns.get((property_name, field_name), None) + return materialized_column + def visit_table_symbol(self, symbol: ast.TableSymbol): return print_hogql_identifier(symbol.table.clickhouse_table()) @@ -351,14 +359,20 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): raise ValueError(f'Can\'t resolve field "{symbol.name}" on table.') field_sql = print_hogql_identifier(resolved_field.name) - if ( - resolved_field.name != symbol.name - or isinstance(symbol.table, ast.TableAliasSymbol) - or symbol_with_name_in_scope != symbol - ): + + # :KLUDGE: Legacy person properties handling. Assume we're in a context where the tables have been joined, + # and this "person_props" alias is accessible to us. + if resolved_field == database.events.person.properties: + if not self.context.using_person_on_events: + field_sql = "person_props" + + # If the field is called on a table that has an alias, prepend the table alias. + # If there's another field with the same name in the scope that's not this, prepend the full table name. + # Note: we don't prepend a table name for the special "person_properties" field. + elif isinstance(symbol.table, ast.TableAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" - # TODO: refactor this lefacy logging + # TODO: refactor this legacy logging if symbol.name != "properties": real_table = symbol.table while isinstance(real_table, ast.TableAliasSymbol): @@ -367,7 +381,9 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): self.context.field_access_logs.append( HogQLFieldAccess( [symbol.name], - "event" if real_table.table == database.events else "person", + cast(Literal["event"], "event") + if real_table.table == database.events + else cast(Literal["person"], "person"), symbol.name, field_sql, ) @@ -404,9 +420,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) - # TODO: cache this - materialized_columns = get_materialized_columns(table_name) - materialized_column = materialized_columns.get((symbol.name, field_name), None) + materialized_column = self._get_materialized_column(table_name, symbol.name, field_name) if materialized_column: property_sql = print_hogql_identifier(materialized_column) @@ -415,7 +429,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") if field_name == "properties": - # TODO: refactor this lefacy logging + # TODO: refactor this legacy logging self.context.field_access_logs.append( HogQLFieldAccess( ["properties", symbol.name], @@ -425,7 +439,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): ) ) elif field_name == "person_properties": - # TODO: refactor this lefacy logging + # TODO: refactor this legacy logging self.context.field_access_logs.append( HogQLFieldAccess( ["person", "properties", symbol.name], diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index ef6bf47e77a7c..81b9e5a326208 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -74,7 +74,7 @@ def test_fields_and_properties(self): context = HogQLContext() self.assertEqual( self._expr("person.properties.bla", context), - "replaceRegexpAll(JSONExtractRaw(events.person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", ) self.assertEqual( context.field_access_logs, @@ -83,7 +83,7 @@ def test_fields_and_properties(self): ["person", "properties", "bla"], "person.properties", "bla", - "replaceRegexpAll(JSONExtractRaw(events.person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", ) ], ) From 30042486eb93580305eea687834cca8c1f165c22 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 08:14:54 +0000 Subject: [PATCH 041/142] Update snapshots --- ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr index 67ba48d323746..4bc6581b95d6e 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr @@ -426,7 +426,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like("mat_$current_url", '%example%'), notEquals('bla', 'a%sd'))) + AND (and(like(mat_$current_url, '%example%'), notEquals('bla', 'a%sd'))) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -576,7 +576,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like("pmat_email", '%test.com')) + AND (like(mat_pp_email, '%test.com')) GROUP BY pdi.person_id) GROUP BY start_of_period, status) From e7efe0a8756899d3c9668e79ce6eada2b1a60b21 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 08:21:33 +0000 Subject: [PATCH 042/142] Update snapshots --- posthog/api/test/__snapshots__/test_insight.ambr | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 6479360ca9019..0aa6381ae13bb 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -207,7 +207,7 @@ /* user_id:0 request:_snapshot_ */ SELECT groupArray(value) FROM - (SELECT if(less(toInt64OrNull("mat_int_value"), 10), 'le%ss', 'more') AS value, + (SELECT if(less(toInt64OrNull(mat_int_value), 10), 'le%ss', 'more') AS value, count(*) as count FROM events e WHERE team_id = 2 @@ -249,13 +249,13 @@ day_start UNION ALL SELECT count(*) as total, toStartOfDay(toTimeZone(toDateTime(timestamp, 'UTC'), 'UTC')) as day_start, - if(less(toInt64OrNull("mat_int_value"), 10), 'le%ss', 'more') as breakdown_value + if(less(toInt64OrNull(mat_int_value), 10), 'le%ss', 'more') as breakdown_value FROM events e WHERE e.team_id = 2 AND event = '$pageview' AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') - AND if(less(toInt64OrNull("mat_int_value"), 10), 'le%ss', 'more') in (['more', 'le%ss']) + AND if(less(toInt64OrNull(mat_int_value), 10), 'le%ss', 'more') in (['more', 'le%ss']) GROUP BY day_start, breakdown_value)) GROUP BY day_start, @@ -395,8 +395,8 @@ AND event = '$pageview' AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') - AND ((and(greater(toInt64OrNull("mat_int_value"), 10), notEquals('bla', 'a%sd'))) - AND (like("pmat_fish", '%fish%'))) + AND ((and(greater(toInt64OrNull(mat_int_value), 10), notEquals('bla', 'a%sd'))) + AND (like(mat_pp_fish, '%fish%'))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -479,8 +479,8 @@ AND event = '$pageview' AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') - AND (and(less(toInt64OrNull("mat_int_value"), 10), notEquals('bla', 'a%sd')) - AND like("pmat_fish", '%fish%')) + AND (and(less(toInt64OrNull(mat_int_value), 10), notEquals('bla', 'a%sd')) + AND like(mat_pp_fish, '%fish%')) GROUP BY date) GROUP BY day_start ORDER BY day_start) From b075bc27df75fed4d1d3647778d11cccaec2446b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 09:52:03 +0100 Subject: [PATCH 043/142] simple splash and table property printing --- posthog/hogql/ast.py | 4 ++++ posthog/hogql/database.py | 20 ++++++++++++++--- posthog/hogql/printer.py | 36 +++++++++++++++++++++--------- posthog/hogql/resolver.py | 18 ++++++++++----- posthog/hogql/test/test_printer.py | 3 +-- 5 files changed, 61 insertions(+), 20 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index eb18afdda34a5..2d55753b9d851 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -118,6 +118,10 @@ class ConstantSymbol(Symbol): value: Any +class SplashSymbol(Symbol): + table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + + class FieldSymbol(Symbol): name: str table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index dc9c8bf87d3ed..7341258bb58bc 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,3 +1,5 @@ +from typing import List + from pydantic import BaseModel, Extra @@ -46,6 +48,18 @@ def get_field(self, name: str) -> DatabaseField: def clickhouse_table(self): raise NotImplementedError("Table.clickhouse_table not overridden") + def get_splash(self) -> List[str]: + list: List[str] = [] + for field in self.__fields__.values(): + database_field = field.default + if isinstance(database_field, DatabaseField): + list.append(database_field.name) + elif isinstance(database_field, Table): + list.extend(database_field.get_splash()) + else: + raise ValueError(f"Unknown field type {type(database_field).__name__} for splash") + return list + class PersonsTable(Table): id: StringDatabaseField = StringDatabaseField(name="id") @@ -84,12 +98,12 @@ def clickhouse_table(self): class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") event: StringDatabaseField = StringDatabaseField(name="event") - timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamp") properties: StringJSONDatabaseField = StringJSONDatabaseField(name="properties") + timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamp") + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") - team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") person: EventsPersonSubTable = EventsPersonSubTable() def clickhouse_table(self): diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4511cd4b9b601..6f836f4ee5672 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -5,7 +5,7 @@ from posthog.hogql import ast from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess -from posthog.hogql.database import database +from posthog.hogql.database import Table, database from posthog.hogql.print_string import print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name from posthog.hogql.visitor import Visitor @@ -267,15 +267,7 @@ def visit_field(self, node: ast.Field): # When printing HogQL, we print the properties out as a chain as they are. return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) - if node.chain == ["*"]: - # query = f"tuple({','.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" - # return self.visit(parse_expr(query)) - raise ValueError("Selecting * not yet implemented") - elif node.chain == ["person"]: - # query = "tuple(distinct_id, person.id, person.created_at, person.properties.name, person.properties.email)" - # return self.visit(parse_expr(query)) - raise ValueError("Selecting person not yet implemented") - elif node.symbol is not None: + if node.symbol is not None: select_query = self._last_select() select: Optional[ast.SelectQuerySymbol] = select_query.symbol if select_query else None if select is None: @@ -357,6 +349,18 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): resolved_field = symbol.resolve_database_field() if resolved_field is None: raise ValueError(f'Can\'t resolve field "{symbol.name}" on table.') + if isinstance(resolved_field, Table): + # :KLUDGE: only works for events.person.* printing now + if isinstance(symbol.table, ast.TableSymbol): + return self.visit(ast.SplashSymbol(table=ast.TableSymbol(table=resolved_field))) + else: + return self.visit( + ast.SplashSymbol( + table=ast.TableAliasSymbol( + table=ast.TableSymbol(table=resolved_field), name=symbol.table.name + ) + ) + ) field_sql = print_hogql_identifier(resolved_field.name) @@ -457,6 +461,18 @@ def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return print_hogql_identifier(symbol.name) + def visit_splash_symbol(self, symbol: ast.SplashSymbol): + table = symbol.table + while isinstance(table, ast.TableAliasSymbol): + table = table.table + if not isinstance(table, ast.TableSymbol): + raise ValueError(f"Unknown SplashSymbol table type: {type(table).__name__}") + splash_fields = table.table.get_splash() + prefix = ( + f"{print_hogql_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" + ) + return f"tuple({', '.join(f'{prefix}{print_hogql_identifier(field)}' for field in splash_fields)})" + def visit_unknown(self, symbol: ast.AST): raise ValueError(f"Unknown Symbol {type(symbol).__name__}") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index b9ecb1c00bcdb..e08e6215a64ed 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -146,14 +146,12 @@ def visit_field(self, node): if len(node.chain) == 0: raise Exception("Invalid field access with empty chain") - # Only look for fields in the last SELECT scope. - scope = self.scopes[-1] - - # ClickHouse does not support subqueries accessing "x.event". This is forbidden: + # Only look for fields in the last SELECT scope, instead of all previous scopes. + # That's because ClickHouse does not support subqueries accessing "x.event". This is forbidden: # - "SELECT event, (select count() from events where event = x.event) as c FROM events x where event = '$pageview'", # But this is supported: # - "SELECT t.big_count FROM (select count() + 100 as big_count from events) as t JOIN events e ON (e.event = t.event)", - # Thus we don't have to recursively look into all the past scopes to find a match. + scope = self.scopes[-1] symbol: Optional[ast.Symbol] = None name = node.chain[0] @@ -161,6 +159,16 @@ def visit_field(self, node): # If the field contains at least two parts, the first might be a table. if len(node.chain) > 1 and name in scope.tables: symbol = scope.tables[name] + + if name == "*" and len(node.chain) == 1: + table_count = len(scope.anonymous_tables) + len(scope.tables) + if table_count == 0: + raise ResolverException("Cannot use '*' when there are no tables in the query") + if table_count > 1: + raise ResolverException("Cannot use '*' when there are multiple tables in the query") + table = scope.anonymous_tables[0] if len(scope.anonymous_tables) > 0 else list(scope.tables.values())[0] + symbol = ast.SplashSymbol(table=table) + if not symbol: symbol = lookup_field_by_name(scope, name) if not symbol: diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 81b9e5a326208..52a4f58daca3b 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -336,9 +336,8 @@ def test_special_root_properties(self): context = HogQLContext() self.assertEqual( self._expr("person", context), - "tuple(distinct_id, person_id, person_created_at, replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', ''), replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_1)s), '^\"|\"$', ''))", + "tuple(person_id, person_created_at, person_properties)", ) - self.assertEqual(context.values, {"hogql_val_0": "name", "hogql_val_1": "email"}) def test_values(self): context = HogQLContext() From e59ef94f7362b4c87cb618ab324666519ad4e25e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 09:11:56 +0000 Subject: [PATCH 044/142] Update snapshots --- .../api/test/__snapshots__/test_query.ambr | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index de06707004e26..9b049fa990e6c 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -50,9 +50,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -65,9 +65,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -81,9 +81,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -160,9 +160,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -175,9 +175,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -191,9 +191,9 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' @@ -207,13 +207,13 @@ /* user_id:0 request:_snapshot_ */ SELECT event, distinct_id, - "mat_key", + mat_key, 'a%sd', - concat(event, ' ', "mat_key") + concat(event, ' ', mat_key) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' - AND equals("mat_key", 'test_val2') + AND equals(mat_key, 'test_val2') ORDER BY event ASC LIMIT 101 ' @@ -283,12 +283,12 @@ # name: TestQuery.test_property_filter_aggregations_materialized ' /* user_id:0 request:_snapshot_ */ - SELECT "mat_key", + SELECT mat_key, count(*) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' - GROUP BY "mat_key" + GROUP BY mat_key ORDER BY count() DESC LIMIT 101 ' @@ -296,12 +296,12 @@ # name: TestQuery.test_property_filter_aggregations_materialized.1 ' /* user_id:0 request:_snapshot_ */ - SELECT "mat_key", + SELECT mat_key, count(*) FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' - GROUP BY "mat_key" + GROUP BY mat_key HAVING greater(count(*), 1) ORDER BY count() DESC LIMIT 101 From f095fec49a64178e45e12521e998768d23bdf053 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 10:32:42 +0100 Subject: [PATCH 045/142] fix pp --- posthog/hogql/printer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 6f836f4ee5672..fd60b538b7af2 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -329,9 +329,13 @@ def __init__(self, select: ast.SelectQuerySymbol, context: HogQLContext): def _get_materialized_column( self, table_name: TablesWithMaterializedColumns, property_name: PropertyName, field_name: TableColumn ) -> Optional[str]: + # :KLUDGE: person property materialised columns support when person on events is off + if not self.context.using_person_on_events and table_name == "events" and field_name == "person_properties": + materialized_columns = get_materialized_columns("person") + return materialized_columns.get(("properties", field_name), None) + materialized_columns = get_materialized_columns(table_name) - materialized_column = materialized_columns.get((property_name, field_name), None) - return materialized_column + return materialized_columns.get((property_name, field_name), None) def visit_table_symbol(self, symbol: ast.TableSymbol): return print_hogql_identifier(symbol.table.clickhouse_table()) From 4715e8a31154e32922e10bb0644315c1de2cc31a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 11:17:09 +0100 Subject: [PATCH 046/142] revert what was different about hogql access logs --- posthog/hogql/printer.py | 17 ++++++++++++----- posthog/hogql/test/test_printer.py | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index fd60b538b7af2..187c66f7a9e74 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -373,10 +373,14 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if resolved_field == database.events.person.properties: if not self.context.using_person_on_events: field_sql = "person_props" + elif resolved_field == database.events.person.id: + pass + elif resolved_field == database.events.person.created_at: + pass # If the field is called on a table that has an alias, prepend the table alias. # If there's another field with the same name in the scope that's not this, prepend the full table name. - # Note: we don't prepend a table name for the special "person_properties" field. + # Note: we don't prepend a table name for the special "person" fields. elif isinstance(symbol.table, ast.TableAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" @@ -386,12 +390,15 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): while isinstance(real_table, ast.TableAliasSymbol): real_table = real_table.table + access_table = ( + cast(Literal["event"], "event") + if real_table.table == database.events + else cast(Literal["person"], "person") + ) self.context.field_access_logs.append( HogQLFieldAccess( - [symbol.name], - cast(Literal["event"], "event") - if real_table.table == database.events - else cast(Literal["person"], "person"), + ["person", symbol.name] if access_table == "person" else [symbol.name], + access_table, symbol.name, field_sql, ) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 52a4f58daca3b..722f2d62512f7 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -109,17 +109,17 @@ def test_fields_and_properties(self): ) context = HogQLContext() - self.assertEqual(self._expr("person.id", context), "events.person_id") + self.assertEqual(self._expr("person.id", context), "person_id") self.assertEqual( context.field_access_logs, - [HogQLFieldAccess(["id"], "person", "id", "events.person_id")], + [HogQLFieldAccess(["person", "id"], "person", "id", "person_id")], ) context = HogQLContext() - self.assertEqual(self._expr("person.created_at", context), "events.person_created_at") + self.assertEqual(self._expr("person.created_at", context), "person_created_at") self.assertEqual( context.field_access_logs, - [HogQLFieldAccess(["created_at"], "person", "created_at", "events.person_created_at")], + [HogQLFieldAccess(["person", "created_at"], "person", "created_at", "person_created_at")], ) def test_hogql_properties(self): From 07204aa18fe24d7152f4d0abdf73d7d8c81eec0d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 13:41:13 +0100 Subject: [PATCH 047/142] explicit names for person non properties --- posthog/hogql/printer.py | 4 ---- posthog/hogql/test/test_printer.py | 8 ++++---- posthog/models/property/util.py | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 187c66f7a9e74..4ba125a20da38 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -373,10 +373,6 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if resolved_field == database.events.person.properties: if not self.context.using_person_on_events: field_sql = "person_props" - elif resolved_field == database.events.person.id: - pass - elif resolved_field == database.events.person.created_at: - pass # If the field is called on a table that has an alias, prepend the table alias. # If there's another field with the same name in the scope that's not this, prepend the full table name. diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 722f2d62512f7..3fe1d322c8bc5 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -109,17 +109,17 @@ def test_fields_and_properties(self): ) context = HogQLContext() - self.assertEqual(self._expr("person.id", context), "person_id") + self.assertEqual(self._expr("person.id", context), "events.person_id") self.assertEqual( context.field_access_logs, - [HogQLFieldAccess(["person", "id"], "person", "id", "person_id")], + [HogQLFieldAccess(["person", "id"], "person", "id", "events.person_id")], ) context = HogQLContext() - self.assertEqual(self._expr("person.created_at", context), "person_created_at") + self.assertEqual(self._expr("person.created_at", context), "events.person_created_at") self.assertEqual( context.field_access_logs, - [HogQLFieldAccess(["person", "created_at"], "person", "created_at", "person_created_at")], + [HogQLFieldAccess(["person", "created_at"], "person", "created_at", "events.person_created_at")], ) def test_hogql_properties(self): diff --git a/posthog/models/property/util.py b/posthog/models/property/util.py index 4dc3952c1becc..1a0b34f5e989a 100644 --- a/posthog/models/property/util.py +++ b/posthog/models/property/util.py @@ -772,6 +772,7 @@ def extract_tables_and_properties(props: List[Property]) -> TCounter[PropertyIde for prop in props: if prop.type == "hogql": context = HogQLContext() + # TODO: Refactor this. Currently it prints and discards a query, just to check the properties. translate_hogql(prop.key, context) for field_access in context.field_access_logs: if field_access.type == "event.properties": From 10023828f4d2c2a1f0a9310dbe97bca1001e0b35 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 13:50:14 +0100 Subject: [PATCH 048/142] consolidate into print_ast --- posthog/hogql/hogql.py | 3 --- posthog/hogql/printer.py | 27 ++++++++++++++++++++++++--- posthog/hogql/query.py | 4 +--- posthog/hogql/visitor.py | 5 +++++ 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 541d0199b4c1f..e96134efda96f 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -5,7 +5,6 @@ from posthog.hogql.database import database from posthog.hogql.parser import parse_expr, parse_select from posthog.hogql.printer import print_ast -from posthog.hogql.resolver import resolve_symbols def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", "clickhouse"] = "clickhouse") -> str: @@ -17,7 +16,6 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", if context.select_team_id: # Only parse full SELECT statements if we have a team_id in the context. node = parse_select(query, no_placeholders=True) - resolve_symbols(node) return print_ast(node, context, dialect, stack=[]) else: # Create a fake query that selects from "events". Assume were in its scope when evaluating expressions. @@ -25,7 +23,6 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", select_query = ast.SelectQuery(select=[], symbol=symbol) node = parse_expr(query, no_placeholders=True) - resolve_symbols(node, symbol) return print_ast(node, context, dialect, stack=[select_query]) except SyntaxError as err: diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4ba125a20da38..fcdf938b4c746 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -7,8 +7,8 @@ from posthog.hogql.context import HogQLContext, HogQLFieldAccess from posthog.hogql.database import Table, database from posthog.hogql.print_string import print_hogql_identifier -from posthog.hogql.resolver import ResolverException, lookup_field_by_name -from posthog.hogql.visitor import Visitor +from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols +from posthog.hogql.visitor import Visitor, clone_expr from posthog.models.property import PropertyName, TableColumn @@ -27,8 +27,25 @@ def team_id_guard_for_table( def print_ast( - node: ast.AST, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: Optional[List[ast.AST]] = None + node: ast.Expr, + context: HogQLContext, + dialect: Literal["hogql", "clickhouse"], + stack: Optional[List[ast.Expr]] = None, ) -> str: + """Print an AST into a string. Does not modify the node.""" + symbol = stack[-1].symbol if stack else None + + # make a clean copy of the object + node = clone_expr(node) + # resolve symbols + resolve_symbols(node, symbol) + + # modify the cloned tree as needed + if dialect == "clickhouse": + # TODO: add team_id checks (currently done in the printer) + # TODO: add joins to person and group tables + pass + return Printer(context=context, dialect=dialect, stack=stack or []).visit(node) @@ -39,6 +56,8 @@ class JoinExprResponse: class Printer(Visitor): + # NOTE: Call "print_ast()", not this class directly. + def __init__( self, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], stack: Optional[List[ast.AST]] = None ): @@ -152,12 +171,14 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: select_from.append(f"AS {print_hogql_identifier(node.alias)}") 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.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) 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.symbol, self.context) elif isinstance(node.symbol, ast.SelectQuerySymbol): diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index 7938da035b6fd..e1979d05706d3 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -8,7 +8,6 @@ from posthog.hogql.parser import parse_select from posthog.hogql.placeholders import assert_no_placeholders, replace_placeholders from posthog.hogql.printer import print_ast -from posthog.hogql.resolver import resolve_symbols from posthog.models import Team from posthog.queries.insight import insight_sync_execute @@ -46,8 +45,7 @@ def execute_hogql_query( if select_query.limit is None: select_query.limit = ast.Constant(value=1000) - hogql_context = HogQLContext(select_team_id=team.pk) - resolve_symbols(select_query) + hogql_context = HogQLContext(select_team_id=team.pk, using_person_on_events=team.person_on_events_querying_enabled) clickhouse = print_ast(select_query, hogql_context, "clickhouse") hogql = print_ast(select_query, hogql_context, "hogql") diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index b5628f5d7287c..e6d94d2ed9ad4 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -1,6 +1,11 @@ from posthog.hogql import ast +def clone_expr(self: ast.Expr) -> ast.Expr: + """Clone an expression node. Removes all symbols.""" + return CloningVisitor().visit(self) + + class Visitor(object): def visit(self, node: ast.AST): if node is None: From 1d681f834b078dc01bd7e2bced450c541cceb410 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 14:09:48 +0100 Subject: [PATCH 049/142] merge symbol printer into printer --- posthog/hogql/hogql.py | 6 +- posthog/hogql/print_string.py | 11 +++- posthog/hogql/printer.py | 93 +++++++++++++++--------------- posthog/hogql/test/test_printer.py | 11 ++-- 4 files changed, 65 insertions(+), 56 deletions(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index e96134efda96f..f39798ae9347c 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -19,9 +19,9 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", return print_ast(node, context, dialect, stack=[]) else: # Create a fake query that selects from "events". Assume were in its scope when evaluating expressions. - symbol = ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) - select_query = ast.SelectQuery(select=[], symbol=symbol) - + select_query = ast.SelectQuery( + select=[], symbol=ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) + ) node = parse_expr(query, no_placeholders=True) return print_ast(node, context, dialect, stack=[select_query]) diff --git a/posthog/hogql/print_string.py b/posthog/hogql/print_string.py index 7b227acf97d82..351bad47457b4 100644 --- a/posthog/hogql/print_string.py +++ b/posthog/hogql/print_string.py @@ -15,9 +15,18 @@ } -# Copied from clickhouse_driver.util.escape, adapted from single quotes to backquotes. +# Copied from clickhouse_driver.util.escape, adapted from single quotes to backquotes. Added a $. def print_hogql_identifier(identifier: str) -> str: + # HogQL allows dollars in the identifier. if re.match(r"^[A-Za-z_$][A-Za-z0-9_$]*$", identifier): return identifier return "`%s`" % "".join(backquote_escape_chars_map.get(c, c) for c in identifier) + + +# Copied from clickhouse_driver.util.escape, adapted from single quotes to backquotes. +def print_clickhouse_identifier(identifier: str) -> str: + if re.match(r"^[A-Za-z_][A-Za-z0-9_]*$", identifier): + return identifier + + return "`%s`" % "".join(backquote_escape_chars_map.get(c, c) for c in identifier) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index fcdf938b4c746..9298313474ae1 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -6,7 +6,7 @@ from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS from posthog.hogql.context import HogQLContext, HogQLFieldAccess from posthog.hogql.database import Table, database -from posthog.hogql.print_string import print_hogql_identifier +from posthog.hogql.print_string import print_clickhouse_identifier, print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols from posthog.hogql.visitor import Visitor, clone_expr from posthog.models.property import PropertyName, TableColumn @@ -66,13 +66,6 @@ def __init__( # Keep track of all traversed nodes. self.stack: List[ast.AST] = stack or [] - def _last_select(self) -> Optional[ast.SelectQuery]: - """Find the last SELECT query in the stack.""" - for node in reversed(self.stack): - if isinstance(node, ast.SelectQuery): - return node - return None - def visit(self, node: ast.AST): self.stack.append(node) response = super().visit(node) @@ -166,16 +159,16 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: raise ValueError(f"Table alias {node.symbol.name} does not resolve!") if not isinstance(table_symbol, ast.TableSymbol): raise ValueError(f"Table alias {node.symbol.name} does not resolve to a table!") - select_from.append(print_hogql_identifier(table_symbol.table.clickhouse_table())) + select_from.append(self._print_identifier(table_symbol.table.clickhouse_table())) if node.alias is not None: - select_from.append(f"AS {print_hogql_identifier(node.alias)}") + select_from.append(f"AS {self._print_identifier(node.alias)}") 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.symbol, self.context) elif isinstance(node.symbol, ast.TableSymbol): - select_from.append(print_hogql_identifier(node.symbol.table.clickhouse_table())) + select_from.append(self._print_identifier(node.symbol.table.clickhouse_table())) if self.dialect == "clickhouse": # TODO: do this in a separate pass before printing, along with person joins and other transforms @@ -186,7 +179,7 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: elif isinstance(node.symbol, ast.SelectQueryAliasSymbol) and node.alias is not None: select_from.append(self.visit(node.table)) - select_from.append(f"AS {print_hogql_identifier(node.alias)}") + select_from.append(f"AS {self._print_identifier(node.alias)}") else: raise ValueError("Only selecting from a table or a subquery is supported") @@ -280,20 +273,20 @@ def visit_constant(self, node: ast.Constant): ) def visit_field(self, node: ast.Field): - original_field = ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) + original_field = ".".join([self._print_identifier(identifier) for identifier in node.chain]) if node.symbol is None: raise ValueError(f"Field {original_field} has no symbol") if self.dialect == "hogql": # When printing HogQL, we print the properties out as a chain as they are. - return ".".join([print_hogql_identifier(identifier) for identifier in node.chain]) + return ".".join([self._print_identifier(identifier) for identifier in node.chain]) if node.symbol is not None: select_query = self._last_select() select: Optional[ast.SelectQuerySymbol] = select_query.symbol if select_query else None if select is None: raise ValueError(f"Can't find SelectQuerySymbol for field: {original_field}") - return SymbolPrinter(select=select, context=self.context).visit(node.symbol) + return self.visit(node.symbol) else: raise ValueError(f"Unknown Symbol, can not print {type(node.symbol).__name__}") @@ -336,37 +329,18 @@ def visit_placeholder(self, node: ast.Placeholder): raise ValueError(f"Found a Placeholder {{{node.field}}} in the tree. Can't generate query!") def visit_alias(self, node: ast.Alias): - return f"{self.visit(node.expr)} AS {print_hogql_identifier(node.alias)}" - - def visit_unknown(self, node: ast.AST): - raise ValueError(f"Unknown AST node {type(node).__name__}") - - -class SymbolPrinter(Visitor): - def __init__(self, select: ast.SelectQuerySymbol, context: HogQLContext): - self.select = select - self.context = context - - def _get_materialized_column( - self, table_name: TablesWithMaterializedColumns, property_name: PropertyName, field_name: TableColumn - ) -> Optional[str]: - # :KLUDGE: person property materialised columns support when person on events is off - if not self.context.using_person_on_events and table_name == "events" and field_name == "person_properties": - materialized_columns = get_materialized_columns("person") - return materialized_columns.get(("properties", field_name), None) - - materialized_columns = get_materialized_columns(table_name) - return materialized_columns.get((property_name, field_name), None) + return f"{self.visit(node.expr)} AS {self._print_identifier(node.alias)}" def visit_table_symbol(self, symbol: ast.TableSymbol): - return print_hogql_identifier(symbol.table.clickhouse_table()) + return self._print_identifier(symbol.table.clickhouse_table()) def visit_table_alias_symbol(self, symbol: ast.TableAliasSymbol): - return print_hogql_identifier(symbol.name) + return self._print_identifier(symbol.name) def visit_field_symbol(self, symbol: ast.FieldSymbol): try: - symbol_with_name_in_scope = lookup_field_by_name(self.select, symbol.name) + last_select = self._last_select() + symbol_with_name_in_scope = lookup_field_by_name(last_select.symbol, symbol.name) if last_select else None except ResolverException: symbol_with_name_in_scope = None @@ -387,7 +361,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): ) ) - field_sql = print_hogql_identifier(resolved_field.name) + field_sql = self._print_identifier(resolved_field.name) # :KLUDGE: Legacy person properties handling. Assume we're in a context where the tables have been joined, # and this "person_props" alias is accessible to us. @@ -422,7 +396,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): ) elif isinstance(symbol.table, ast.SelectQuerySymbol) or isinstance(symbol.table, ast.SelectQueryAliasSymbol): - field_sql = print_hogql_identifier(symbol.name) + field_sql = self._print_identifier(symbol.name) if isinstance(symbol.table, ast.SelectQueryAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" @@ -455,7 +429,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): materialized_column = self._get_materialized_column(table_name, symbol.name, field_name) if materialized_column: - property_sql = print_hogql_identifier(materialized_column) + property_sql = self._print_identifier(materialized_column) else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") @@ -484,10 +458,10 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): return property_sql def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): - return print_hogql_identifier(symbol.name) + return self._print_identifier(symbol.name) def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): - return print_hogql_identifier(symbol.name) + return self._print_identifier(symbol.name) def visit_splash_symbol(self, symbol: ast.SplashSymbol): table = symbol.table @@ -497,12 +471,35 @@ def visit_splash_symbol(self, symbol: ast.SplashSymbol): raise ValueError(f"Unknown SplashSymbol table type: {type(table).__name__}") splash_fields = table.table.get_splash() prefix = ( - f"{print_hogql_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" + f"{self._print_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" ) - return f"tuple({', '.join(f'{prefix}{print_hogql_identifier(field)}' for field in splash_fields)})" + return f"tuple({', '.join(f'{prefix}{self._print_identifier(field)}' for field in splash_fields)})" + + def visit_unknown(self, node: ast.AST): + raise ValueError(f"Unknown AST node {type(node).__name__}") + + def _last_select(self) -> Optional[ast.SelectQuery]: + """Find the last SELECT query in the stack.""" + for node in reversed(self.stack): + if isinstance(node, ast.SelectQuery): + return node + return None - def visit_unknown(self, symbol: ast.AST): - raise ValueError(f"Unknown Symbol {type(symbol).__name__}") + def _print_identifier(self, name: str) -> str: + if self.dialect == "clickhouse": + return print_clickhouse_identifier(name) + return print_hogql_identifier(name) + + def _get_materialized_column( + self, table_name: TablesWithMaterializedColumns, property_name: PropertyName, field_name: TableColumn + ) -> Optional[str]: + # :KLUDGE: person property materialised columns support when person on events is off + if not self.context.using_person_on_events and table_name == "events" and field_name == "person_properties": + materialized_columns = get_materialized_columns("person") + return materialized_columns.get(("properties", field_name), None) + + materialized_columns = get_materialized_columns(table_name) + return materialized_columns.get((property_name, field_name), None) def trim_quotes_expr(expr: str) -> str: diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 3fe1d322c8bc5..6a8ec04c86290 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -172,16 +172,19 @@ def test_materialized_fields_and_properties(self): self.assertEqual(1 + 2, 3) return materialize("events", "$browser") - self.assertEqual(self._expr("properties['$browser']"), "mat_$browser") + self.assertEqual(self._expr("properties['$browser']"), "`mat_$browser`") + + materialize("events", "withoutdollar") + self.assertEqual(self._expr("properties['withoutdollar']"), "mat_withoutdollar") materialize("events", "$browser and string") - self.assertEqual(self._expr("properties['$browser and string']"), "mat_$browser_and_string") + self.assertEqual(self._expr("properties['$browser and string']"), "`mat_$browser_and_string`") materialize("events", "$browser%%%#@!@") - self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "mat_$browser_______") + self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "`mat_$browser_______`") materialize("events", "$initial_waffle", table_column="person_properties") - self.assertEqual(self._expr("person.properties['$initial_waffle']"), "mat_pp_$initial_waffle") + self.assertEqual(self._expr("person.properties['$initial_waffle']"), "`mat_pp_$initial_waffle`") def test_methods(self): self.assertEqual(self._expr("count()"), "count(*)") From b32cb4b682dfcaace53b94860f8a10ee4cdc7c8b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 15 Feb 2023 14:29:07 +0100 Subject: [PATCH 050/142] move it up --- posthog/hogql/printer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 9298313474ae1..bce2d1a9b25b3 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -426,7 +426,11 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) - materialized_column = self._get_materialized_column(table_name, symbol.name, field_name) + if field_name == "person_properties" and not self.context.using_person_on_events: + # :KLUDGE: person property materialized columns support when person on events is off + materialized_column = self._get_materialized_column("person", symbol.name, "properties") + else: + materialized_column = self._get_materialized_column(table_name, symbol.name, field_name) if materialized_column: property_sql = self._print_identifier(materialized_column) @@ -493,11 +497,6 @@ def _print_identifier(self, name: str) -> str: def _get_materialized_column( self, table_name: TablesWithMaterializedColumns, property_name: PropertyName, field_name: TableColumn ) -> Optional[str]: - # :KLUDGE: person property materialised columns support when person on events is off - if not self.context.using_person_on_events and table_name == "events" and field_name == "person_properties": - materialized_columns = get_materialized_columns("person") - return materialized_columns.get(("properties", field_name), None) - materialized_columns = get_materialized_columns(table_name) return materialized_columns.get((property_name, field_name), None) From 425a02e216ee5094f0ad88d019443afe8a272bcf Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 13:34:51 +0000 Subject: [PATCH 051/142] Update snapshots --- ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr index 4bc6581b95d6e..90a37c4108324 100644 --- a/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr +++ b/ee/clickhouse/queries/test/__snapshots__/test_lifecycle.ambr @@ -426,7 +426,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (and(like(mat_$current_url, '%example%'), notEquals('bla', 'a%sd'))) + AND (and(like(`mat_$current_url`, '%example%'), notEquals('bla', 'a%sd'))) GROUP BY pdi.person_id) GROUP BY start_of_period, status) @@ -576,7 +576,7 @@ AND event = '$pageview' AND timestamp >= toDateTime(dateTrunc('day', toDateTime('2021-04-28 00:00:00', 'UTC'))) - INTERVAL 1 day AND timestamp < toDateTime(dateTrunc('day', toDateTime('2021-05-05 23:59:59', 'UTC'))) + INTERVAL 1 day - AND (like(mat_pp_email, '%test.com')) + AND (like(pmat_email, '%test.com')) GROUP BY pdi.person_id) GROUP BY start_of_period, status) From 0d17eb75c3e1ab31d7db0203fa1425861855906a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 13:41:32 +0000 Subject: [PATCH 052/142] Update snapshots --- posthog/api/test/__snapshots__/test_insight.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index 0aa6381ae13bb..c21dfe15be9df 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -396,7 +396,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND ((and(greater(toInt64OrNull(mat_int_value), 10), notEquals('bla', 'a%sd'))) - AND (like(mat_pp_fish, '%fish%'))) + AND (like(pmat_fish, '%fish%'))) GROUP BY date) GROUP BY day_start ORDER BY day_start) @@ -480,7 +480,7 @@ AND toTimeZone(timestamp, 'UTC') >= toDateTime(toStartOfDay(toDateTime('2012-01-08 00:00:00', 'UTC')), 'UTC') AND toTimeZone(timestamp, 'UTC') <= toDateTime('2012-01-15 23:59:59', 'UTC') AND (and(less(toInt64OrNull(mat_int_value), 10), notEquals('bla', 'a%sd')) - AND like(mat_pp_fish, '%fish%')) + AND like(pmat_fish, '%fish%')) GROUP BY date) GROUP BY day_start ORDER BY day_start) From 4851b8f2b180c3dd2dc5e9d5fc3eaca3b391c12e Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Feb 2023 13:52:49 +0000 Subject: [PATCH 053/142] Update snapshots --- .../__snapshots__/test_session_recording_list.ambr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr b/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr index 1a633f0a9edbb..79070c7c9eaf0 100644 --- a/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr +++ b/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr @@ -483,14 +483,14 @@ any(session_recordings.duration) as duration, any(session_recordings.distinct_id) as distinct_id , countIf(event = '$pageview' - AND (equals("mat_$browser", 'Chrome') - AND equals("pmat_email", 'bla'))) as count_event_match_0 , + AND (equals(`mat_$browser`, 'Chrome') + AND equals(pmat_email, 'bla'))) as count_event_match_0 , groupUniqArrayIf(100)((events.timestamp, events.uuid, events.session_id, events.window_id), event = '$pageview' - AND (equals("mat_$browser", 'Chrome') - AND equals("pmat_email", 'bla'))) as matching_events_0 + AND (equals(`mat_$browser`, 'Chrome') + AND equals(pmat_email, 'bla'))) as matching_events_0 FROM (SELECT uuid, distinct_id, @@ -562,12 +562,12 @@ any(session_recordings.duration) as duration, any(session_recordings.distinct_id) as distinct_id , countIf(event = '$pageview' - AND (equals("mat_$browser", 'Firefox'))) as count_event_match_0 , + AND (equals(`mat_$browser`, 'Firefox'))) as count_event_match_0 , groupUniqArrayIf(100)((events.timestamp, events.uuid, events.session_id, events.window_id), event = '$pageview' - AND (equals("mat_$browser", 'Firefox'))) as matching_events_0 + AND (equals(`mat_$browser`, 'Firefox'))) as matching_events_0 FROM (SELECT uuid, distinct_id, From d17d72578d9e879313fa35977ff3de870e465da1 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 10:31:54 +0100 Subject: [PATCH 054/142] basic hogql node --- .../lib/lemon-ui/LemonTable/LemonTable.tsx | 2 +- frontend/src/queries/examples.ts | 17 +++++++ .../src/queries/nodes/DataNode/DataNode.tsx | 4 +- .../queries/nodes/DataNode/dataNodeLogic.ts | 15 +++++- .../src/queries/nodes/DataTable/DataTable.tsx | 26 ++++++++-- .../queries/nodes/DataTable/dataTableLogic.ts | 7 +-- frontend/src/queries/query.ts | 5 ++ frontend/src/queries/schema.json | 51 +++++++++++++++++++ frontend/src/queries/schema.ts | 19 ++++++- frontend/src/queries/utils.ts | 7 ++- .../scenes/saved-insights/SavedInsights.tsx | 6 +++ posthog/api/query.py | 49 +++++++++--------- posthog/api/test/test_query.py | 47 ++++++++++++++++- posthog/schema.py | 25 ++++++++- 14 files changed, 236 insertions(+), 44 deletions(-) diff --git a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx index aeffd4a92400b..f800eb7f840ad 100644 --- a/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx +++ b/frontend/src/lib/lemon-ui/LemonTable/LemonTable.tsx @@ -143,7 +143,7 @@ export function LemonTable>({ ) const columnGroups = ( - 'children' in rawColumns[0] + rawColumns.length > 0 && 'children' in rawColumns[0] ? rawColumns : [ { diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 87ca2b5d229c8..dd34e5ad1b947 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -5,6 +5,7 @@ import { EventsNode, EventsQuery, FunnelsQuery, + HogQLQuery, LegacyQuery, LifecycleQuery, Node, @@ -290,6 +291,20 @@ const TimeToSeeDataWaterfall: TimeToSeeDataWaterfallNode = { }, } +const HogQL: HogQLQuery = { + kind: NodeKind.HogQLQuery, + query: 'select 1, 2', +} + +const HogQLTable: DataTableNode = { + kind: NodeKind.DataTableNode, + full: true, + source: { + kind: NodeKind.HogQLQuery, + query: 'select 1, 2', + }, +} + export const examples: Record = { Events, EventsTable, @@ -310,6 +325,8 @@ export const examples: Record = { TimeToSeeDataSessions, TimeToSeeDataWaterfall, TimeToSeeDataJSON, + HogQL, + HogQLTable, } export const stringifiedExamples: Record = Object.fromEntries( diff --git a/frontend/src/queries/nodes/DataNode/DataNode.tsx b/frontend/src/queries/nodes/DataNode/DataNode.tsx index 992bc84126299..465651fa1963d 100644 --- a/frontend/src/queries/nodes/DataNode/DataNode.tsx +++ b/frontend/src/queries/nodes/DataNode/DataNode.tsx @@ -18,7 +18,7 @@ let uniqueNode = 0 export function DataNode(props: DataNodeProps): JSX.Element { const [key] = useState(() => `DataNode.${uniqueNode++}`) const logic = dataNodeLogic({ ...props, key }) - const { response, responseLoading } = useValues(logic) + const { response, responseLoading, responseErrorObject } = useValues(logic) return (
@@ -36,7 +36,7 @@ export function DataNode(props: DataNodeProps): JSX.Element { theme="vs-light" className="border" language={'json'} - value={JSON.stringify(response, null, 2)} + value={JSON.stringify(response ?? responseErrorObject, null, 2)} height={Math.max(height, 300)} /> )} diff --git a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts index 44d0a467c599e..9ea069e8800c4 100644 --- a/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts +++ b/frontend/src/queries/nodes/DataNode/dataNodeLogic.ts @@ -166,11 +166,24 @@ export const dataNodeLogic = kea([ // Clear the response if a failure to avoid showing inconsistencies in the UI loadDataFailure: () => null, }, + responseErrorObject: [ + null as Record | null, + { + loadData: () => null, + loadDataFailure: (_, { errorObject }) => errorObject, + loadDataSuccess: () => null, + }, + ], responseError: [ null as string | null, { loadData: () => null, - loadDataFailure: () => 'Error loading data', + loadDataFailure: (_, { error, errorObject }) => { + if (errorObject && 'error' in errorObject) { + return errorObject.error + } + return error ?? 'Error loading data' + }, loadDataSuccess: () => null, }, ], diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 3c9acf2be5fbc..93c71c5602b6a 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -21,7 +21,7 @@ import { EventBufferNotice } from 'scenes/events/EventBufferNotice' import clsx from 'clsx' import { SessionPlayerModal } from 'scenes/session-recordings/player/modal/SessionPlayerModal' import { InlineEditorButton } from '~/queries/nodes/Node/InlineEditorButton' -import { isEventsQuery, isHogQlAggregation, isPersonsNode, taxonomicFilterToHogQl } from '~/queries/utils' +import { isEventsQuery, isHogQlAggregation, isHogQLQuery, isPersonsNode, taxonomicFilterToHogQl } from '~/queries/utils' import { PersonPropertyFilters } from '~/queries/nodes/PersonsNode/PersonPropertyFilters' import { PersonsSearch } from '~/queries/nodes/PersonsNode/PersonsSearch' import { PersonDeleteModal } from 'scenes/persons/PersonDeleteModal' @@ -89,8 +89,9 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele } = queryWithDefaults const actionsColumnShown = showActions && isEventsQuery(query.source) && columnsInResponse?.includes('*') + const columnsInLemonTable = isHogQLQuery(query.source) ? columnsInResponse ?? columnsInQuery : columnsInQuery const lemonColumns: LemonTableColumn[] = [ - ...columnsInQuery.map((key, index) => ({ + ...columnsInLemonTable.map((key, index) => ({ dataIndex: key as any, ...renderColumnMeta(key, query, context), render: function RenderDataTableColumn(_: any, { result, label }: DataTableRow) { @@ -98,13 +99,13 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele if (index === (expandable ? 1 : 0)) { return { children: label, - props: { colSpan: columnsInQuery.length + (actionsColumnShown ? 1 : 0) }, + props: { colSpan: columnsInLemonTable.length + (actionsColumnShown ? 1 : 0) }, } } else { return { props: { colSpan: 0 } } } } else if (result) { - if (isEventsQuery(query.source)) { + if (isEventsQuery(query.source) || isHogQLQuery(query.source)) { return renderColumn(key, result[index], result, query, setQuery, context) } return renderColumn(key, result[key], result, query, setQuery, context) @@ -398,7 +399,22 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele }} sorting={null} useURLForSorting={false} - emptyState={responseError ? : } + emptyState={ + responseError ? ( + isHogQLQuery(query.source) ? ( + + ) : ( + + ) + ) : ( + + ) + } expandable={ expandable && isEventsQuery(query.source) && columnsInResponse?.includes('*') ? { diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index f9193f38c18c2..359a1c136e434 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -59,12 +59,7 @@ export const dataTableLogic = kea([ columnsInResponse: [ (s) => [s.response], (response: AnyDataNode['response']): string[] | null => - response && - 'columns' in response && - Array.isArray(response.columns) && - !response.columns.find((c) => typeof c !== 'string') - ? (response?.columns as string[]) - : null, + response && 'columns' in response && Array.isArray(response.columns) ? response?.columns : null, ], dataTableRows: [ (s) => [s.sourceKind, s.orderBy, s.response, s.columnsInQuery, s.columnsInResponse], diff --git a/frontend/src/queries/query.ts b/frontend/src/queries/query.ts index 9e13bad1b38b8..f9ea3b1cd49aa 100644 --- a/frontend/src/queries/query.ts +++ b/frontend/src/queries/query.ts @@ -9,6 +9,7 @@ import { isRecentPerformancePageViewNode, isDataTableNode, isTimeToSeeDataSessionsNode, + isHogQLQuery, } from './utils' import api, { ApiMethodOptions } from 'lib/api' import { getCurrentTeamId } from 'lib/utils/logics' @@ -46,6 +47,8 @@ export function queryExportContext( after: now().subtract(EVENTS_DAYS_FIRST_FETCH, 'day').toISOString(), }, } + } else if (isHogQLQuery(query)) { + return { path: api.queryURL(), method: 'POST', body: query } } else if (isPersonsNode(query)) { return { path: getPersonsEndpoint(query) } } else if (isInsightQueryNode(query)) { @@ -115,6 +118,8 @@ export async function query( } } return await api.query({ after: now().subtract(1, 'year').toISOString(), ...query }, methodOptions) + } else if (isHogQLQuery(query)) { + return api.query(query, methodOptions) } else if (isPersonsNode(query)) { return await api.get(getPersonsEndpoint(query), methodOptions) } else if (isInsightQueryNode(query)) { diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 58353872860da..837732a4ce7ec 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -80,6 +80,9 @@ }, { "$ref": "#/definitions/PersonsNode" + }, + { + "$ref": "#/definitions/HogQLQuery" } ] }, @@ -1273,6 +1276,9 @@ }, { "$ref": "#/definitions/RecentPerformancePageViewNode" + }, + { + "$ref": "#/definitions/HogQLQuery" } ], "description": "Source of the events" @@ -1944,6 +1950,51 @@ "required": ["key", "type"], "type": "object" }, + "HogQLQuery": { + "additionalProperties": false, + "properties": { + "kind": { + "const": "HogQLQuery", + "type": "string" + }, + "query": { + "type": "string" + }, + "response": { + "$ref": "#/definitions/HogQLQueryResponse", + "description": "Cached query response" + } + }, + "required": ["kind", "query"], + "type": "object" + }, + "HogQLQueryResponse": { + "additionalProperties": false, + "properties": { + "clickhouse": { + "type": "string" + }, + "columns": { + "items": {}, + "type": "array" + }, + "hogql": { + "type": "string" + }, + "query": { + "type": "string" + }, + "results": { + "items": {}, + "type": "array" + }, + "types": { + "items": {}, + "type": "array" + } + }, + "type": "object" + }, "InsightQueryNode": { "anyOf": [ { diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 03f0134ca698c..c36176a99a696 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -40,6 +40,7 @@ export enum NodeKind { NewEntityNode = 'NewEntityNode', EventsQuery = 'EventsQuery', PersonsNode = 'PersonsNode', + HogQLQuery = 'HogQLQuery', // Interface nodes DataTableNode = 'DataTableNode', @@ -64,7 +65,7 @@ export enum NodeKind { RecentPerformancePageViewNode = 'RecentPerformancePageViewNode', } -export type AnyDataNode = EventsNode | EventsQuery | ActionsNode | PersonsNode +export type AnyDataNode = EventsNode | EventsQuery | ActionsNode | PersonsNode | HogQLQuery export type QuerySchema = // Data nodes (see utils.ts) @@ -101,6 +102,20 @@ export interface DataNode extends Node { response?: Record } +export interface HogQLQueryResponse { + query?: string + hogql?: string + clickhouse?: string + results?: any[] + types?: any[] + columns?: any[] +} + +export interface HogQLQuery extends DataNode { + kind: NodeKind.HogQLQuery + query: string + response?: HogQLQueryResponse +} export interface EntityNode extends DataNode { name?: string custom_name?: string @@ -201,7 +216,7 @@ export type HasPropertiesNode = EventsNode | EventsQuery | PersonsNode export interface DataTableNode extends Node { kind: NodeKind.DataTableNode /** Source of the events */ - source: EventsNode | EventsQuery | PersonsNode | RecentPerformancePageViewNode + source: EventsNode | EventsQuery | PersonsNode | RecentPerformancePageViewNode | HogQLQuery /** Columns shown in the table, unless the `source` provides them. */ columns?: HogQLExpression[] /** Columns that aren't shown in the table, even if in columns or returned data */ diff --git a/frontend/src/queries/utils.ts b/frontend/src/queries/utils.ts index a59c909642a32..2baee0972e2c7 100644 --- a/frontend/src/queries/utils.ts +++ b/frontend/src/queries/utils.ts @@ -4,6 +4,7 @@ import { DateRange, EventsNode, EventsQuery, + HogQLQuery, TrendsQuery, FunnelsQuery, RetentionQuery, @@ -27,7 +28,7 @@ import { import { TaxonomicFilterGroupType, TaxonomicFilterValue } from 'lib/components/TaxonomicFilter/types' export function isDataNode(node?: Node): node is EventsQuery | PersonsNode | TimeToSeeDataSessionsQuery { - return isEventsQuery(node) || isPersonsNode(node) || isTimeToSeeDataSessionsQuery(node) + return isEventsQuery(node) || isPersonsNode(node) || isTimeToSeeDataSessionsQuery(node) || isHogQLQuery(node) } export function isEventsNode(node?: Node): node is EventsNode { @@ -58,6 +59,10 @@ export function isLegacyQuery(node?: Node): node is LegacyQuery { return node?.kind === NodeKind.LegacyQuery } +export function isHogQLQuery(node?: Node): node is HogQLQuery { + return node?.kind === NodeKind.HogQLQuery +} + /* * Insight Queries */ diff --git a/frontend/src/scenes/saved-insights/SavedInsights.tsx b/frontend/src/scenes/saved-insights/SavedInsights.tsx index 0e04c893fc2d5..504743a42098d 100644 --- a/frontend/src/scenes/saved-insights/SavedInsights.tsx +++ b/frontend/src/scenes/saved-insights/SavedInsights.tsx @@ -218,6 +218,12 @@ export const QUERY_TYPES_METADATA: Record = { icon: IconCoffee, inMenu: true, }, + [NodeKind.HogQLQuery]: { + name: 'HogQL', + description: 'Direct HogQL query', + icon: IconCoffee, + inMenu: true, + }, } export const INSIGHT_TYPE_OPTIONS: LemonSelectOptions = [ diff --git a/posthog/api/query.py b/posthog/api/query.py index 95909caafd620..17d2dbc4014b9 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -4,6 +4,7 @@ from django.http import HttpResponse, JsonResponse from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import OpenApiParameter +from pydantic import BaseModel from rest_framework import viewsets from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated @@ -11,11 +12,12 @@ from posthog.api.documentation import extend_schema from posthog.api.routing import StructuredViewSetMixin +from posthog.hogql.query import execute_hogql_query from posthog.models import Team from posthog.models.event.query_event_list import run_events_query from posthog.permissions import ProjectMembershipNecessaryPermissions, TeamMemberAccessPermission from posthog.rate_limit import ClickHouseBurstRateThrottle, ClickHouseSustainedRateThrottle -from posthog.schema import EventsQuery +from posthog.schema import EventsQuery, HogQLQuery class QueryViewSet(StructuredViewSetMixin, viewsets.ViewSet): @@ -33,13 +35,27 @@ class QueryViewSet(StructuredViewSetMixin, viewsets.ViewSet): ) def list(self, request: Request, **kw) -> HttpResponse: query_json = self._query_json_from_request(request) - query_result = process_query(self.team, query_json) - return JsonResponse(query_result) + return self._process_query(self.team, query_json) def post(self, request, *args, **kwargs): query_json = request.data - query_result = process_query(self.team, query_json) - return JsonResponse(query_result) + return self._process_query(self.team, query_json) + + def _process_query(self, team: Team, query_json: Dict) -> JsonResponse: + # try: + query_kind = query_json.get("kind") + if query_kind == "EventsQuery": + query = EventsQuery.parse_obj(query_json) + response = run_events_query(query=query, team=team) + return self._response_to_json_response(response) + elif query_kind == "HogQLQuery": + query = HogQLQuery.parse_obj(query_json) + response = execute_hogql_query(query=query.query, team=team) + return self._response_to_json_response(response) + else: + raise ValidationError("Unsupported query kind: %s" % query_kind) + # except Exception as e: + # return JsonResponse({"error": str(e)}, status=400) def _query_json_from_request(self, request): if request.method == "POST": @@ -65,21 +81,8 @@ def parsing_error(ex): raise ValidationError("Invalid JSON: %s" % (str(error_main))) return query - -def process_query(team: Team, query_json: Dict) -> Dict: - query_kind = query_json.get("kind") - if query_kind == "EventsQuery": - query = EventsQuery.parse_obj(query_json) - query_result = run_events_query( - team=team, - query=query, - ) - # :KLUDGE: Calling `query_result.dict()` without the following deconstruction fails with a cryptic error - return { - "columns": query_result.columns, - "types": query_result.types, - "results": query_result.results, - "hasMore": query_result.hasMore, - } - else: - raise ValidationError("Unsupported query kind: %s" % query_kind) + def _response_to_json_response(self, response: BaseModel) -> JsonResponse: + dict = {} + for key in response.__fields__.keys(): + dict[key] = getattr(response, key) + return JsonResponse(dict) diff --git a/posthog/api/test/test_query.py b/posthog/api/test/test_query.py index 4c65e8ee5eec6..8ac341658f368 100644 --- a/posthog/api/test/test_query.py +++ b/posthog/api/test/test_query.py @@ -1,7 +1,15 @@ from freezegun import freeze_time from rest_framework import status -from posthog.schema import EventPropertyFilter, EventsQuery, HogQLPropertyFilter, PersonPropertyFilter, PropertyOperator +from posthog.schema import ( + EventPropertyFilter, + EventsQuery, + HogQLPropertyFilter, + HogQLQuery, + HogQLQueryResponse, + PersonPropertyFilter, + PropertyOperator, +) from posthog.test.base import ( APIBaseTest, ClickhouseTestMixin, @@ -256,3 +264,40 @@ def test_property_filter_aggregations(self): query.where = ["count() > 1"] response = self.client.post(f"/api/projects/{self.team.id}/query/", query.dict()).json() self.assertEqual(len(response["results"]), 1) + + @also_test_with_materialized_columns(event_properties=["key"]) + @snapshot_clickhouse_queries + def test_full_hogql_query(self): + with freeze_time("2020-01-10 12:00:00"): + _create_person( + properties={"email": "tom@posthog.com"}, + distinct_ids=["2", "some-random-uid"], + team=self.team, + immediate=True, + ) + _create_event(team=self.team, event="sign up", distinct_id="2", properties={"key": "test_val1"}) + with freeze_time("2020-01-10 12:11:00"): + _create_event(team=self.team, event="sign out", distinct_id="2", properties={"key": "test_val2"}) + with freeze_time("2020-01-10 12:12:00"): + _create_event(team=self.team, event="sign out", distinct_id="3", properties={"key": "test_val2"}) + with freeze_time("2020-01-10 12:13:00"): + _create_event( + team=self.team, event="sign out", distinct_id="4", properties={"key": "test_val3", "path": "a/b/c"} + ) + flush_persons_and_events() + + with freeze_time("2020-01-10 12:14:00"): + query = HogQLQuery(query="select event, distinct_id, properties.key from events order by timestamp") + api_response = self.client.post(f"/api/projects/{self.team.id}/query/", query.dict()).json() + query.response = HogQLQueryResponse.parse_obj(api_response) + + self.assertEqual(len(query.response.results), 4) + self.assertEqual( + query.response.results, + [ + ["sign up", "2", "test_val1"], + ["sign out", "2", "test_val2"], + ["sign out", "3", "test_val2"], + ["sign out", "4", "test_val3"], + ], + ) diff --git a/posthog/schema.py b/posthog/schema.py index a9c22bf6e7321..f6a89c5905cd7 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -240,6 +240,18 @@ class FunnelCorrelationPersonConverted1(str, Enum): false = "false" +class HogQLQueryResponse(BaseModel): + class Config: + extra = Extra.forbid + + clickhouse: Optional[str] = None + columns: Optional[List] = None + hogql: Optional[str] = None + query: Optional[str] = None + results: Optional[List] = None + types: Optional[List] = None + + class InsightType(str, Enum): TRENDS = "TRENDS" STICKINESS = "STICKINESS" @@ -536,6 +548,15 @@ class Config: value: Optional[Union[str, float, List[Union[str, float]]]] = None +class HogQLQuery(BaseModel): + class Config: + extra = Extra.forbid + + kind: str = Field("HogQLQuery", const=True) + query: str + response: Optional[HogQLQueryResponse] = Field(None, description="Cached query response") + + class LifecycleFilter(BaseModel): class Config: extra = Extra.forbid @@ -828,7 +849,7 @@ class Config: showReload: Optional[bool] = Field(None, description="Show a reload button") showSavedQueries: Optional[bool] = Field(None, description="Shows a list of saved queries") showSearch: Optional[bool] = Field(None, description="Include a free text search field (PersonsNode only)") - source: Union[EventsNode, EventsQuery, PersonsNode, RecentPerformancePageViewNode] = Field( + source: Union[EventsNode, EventsQuery, PersonsNode, RecentPerformancePageViewNode, HogQLQuery] = Field( ..., description="Source of the events" ) @@ -1460,7 +1481,7 @@ class Model(BaseModel): LifecycleQuery, RecentPerformancePageViewNode, TimeToSeeDataSessionsQuery, - Union[EventsNode, EventsQuery, ActionsNode, PersonsNode], + Union[EventsNode, EventsQuery, ActionsNode, PersonsNode, HogQLQuery], ] From cc17eff30d780947644f9549da206616ad9a1faa Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 11:28:00 +0100 Subject: [PATCH 055/142] no need to discard resolved symbols... we use them again and again --- posthog/api/query.py | 28 ++++++++++++++-------------- posthog/hogql/printer.py | 4 +--- posthog/hogql/query.py | 8 +++++++- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/posthog/api/query.py b/posthog/api/query.py index 17d2dbc4014b9..289b1d4d366b2 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -42,20 +42,20 @@ def post(self, request, *args, **kwargs): return self._process_query(self.team, query_json) def _process_query(self, team: Team, query_json: Dict) -> JsonResponse: - # try: - query_kind = query_json.get("kind") - if query_kind == "EventsQuery": - query = EventsQuery.parse_obj(query_json) - response = run_events_query(query=query, team=team) - return self._response_to_json_response(response) - elif query_kind == "HogQLQuery": - query = HogQLQuery.parse_obj(query_json) - response = execute_hogql_query(query=query.query, team=team) - return self._response_to_json_response(response) - else: - raise ValidationError("Unsupported query kind: %s" % query_kind) - # except Exception as e: - # return JsonResponse({"error": str(e)}, status=400) + try: + query_kind = query_json.get("kind") + if query_kind == "EventsQuery": + query = EventsQuery.parse_obj(query_json) + response = run_events_query(query=query, team=team) + return self._response_to_json_response(response) + elif query_kind == "HogQLQuery": + query = HogQLQuery.parse_obj(query_json) + response = execute_hogql_query(query=query.query, team=team) + return self._response_to_json_response(response) + else: + raise ValidationError("Unsupported query kind: %s" % query_kind) + except Exception as e: + return JsonResponse({"error": str(e)}, status=400) def _query_json_from_request(self, request): if request.method == "POST": diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index bce2d1a9b25b3..5f27ff8ce3ba5 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -8,7 +8,7 @@ from posthog.hogql.database import Table, database from posthog.hogql.print_string import print_clickhouse_identifier, print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols -from posthog.hogql.visitor import Visitor, clone_expr +from posthog.hogql.visitor import Visitor from posthog.models.property import PropertyName, TableColumn @@ -35,8 +35,6 @@ def print_ast( """Print an AST into a string. Does not modify the node.""" symbol = stack[-1].symbol if stack else None - # make a clean copy of the object - node = clone_expr(node) # resolve symbols resolve_symbols(node, symbol) diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index e1979d05706d3..45251b69a610b 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -56,7 +56,13 @@ def execute_hogql_query( query_type=query_type, workload=workload, ) - print_columns = [print_ast(col, HogQLContext(), "hogql") for col in select_query.select] + print_columns = [] + for node in select_query.select: + if isinstance(node, ast.Alias): + print_columns.append(node.alias) + else: + print_columns.append(print_ast(node=node, context=hogql_context, dialect="hogql", stack=[select_query])) + return HogQLQueryResponse( query=query, hogql=hogql, From f3e7900d96605d934142c94a6ac00a15e4b75b87 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 11:29:45 +0100 Subject: [PATCH 056/142] actual sql editor --- frontend/src/queries/examples.ts | 7 +-- .../src/queries/nodes/DataTable/DataTable.tsx | 11 +++- .../queries/nodes/DataTable/dataTableLogic.ts | 1 + .../nodes/HogQLQuery/HogQLQueryEditor.tsx | 55 +++++++++++++++++++ .../nodes/HogQLQuery/hogQLQueryEditorLogic.ts | 34 ++++++++++++ frontend/src/queries/schema.ts | 2 + 6 files changed, 102 insertions(+), 8 deletions(-) create mode 100644 frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx create mode 100644 frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index dd34e5ad1b947..91077af07054d 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -293,16 +293,13 @@ const TimeToSeeDataWaterfall: TimeToSeeDataWaterfallNode = { const HogQL: HogQLQuery = { kind: NodeKind.HogQLQuery, - query: 'select 1, 2', + query: 'select event, count() as event_count from events group by event order by event_count desc', } const HogQLTable: DataTableNode = { kind: NodeKind.DataTableNode, full: true, - source: { - kind: NodeKind.HogQLQuery, - query: 'select 1, 2', - }, + source: HogQL, } export const examples: Record = { diff --git a/frontend/src/queries/nodes/DataTable/DataTable.tsx b/frontend/src/queries/nodes/DataTable/DataTable.tsx index 93c71c5602b6a..298fffde66a7b 100644 --- a/frontend/src/queries/nodes/DataTable/DataTable.tsx +++ b/frontend/src/queries/nodes/DataTable/DataTable.tsx @@ -1,5 +1,5 @@ import './DataTable.scss' -import { DataTableNode, EventsNode, EventsQuery, Node, PersonsNode, QueryContext } from '~/queries/schema' +import { DataTableNode, EventsNode, EventsQuery, HogQLQuery, Node, PersonsNode, QueryContext } from '~/queries/schema' import { useCallback, useState } from 'react' import { BindLogic, useValues } from 'kea' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' @@ -34,6 +34,7 @@ import { extractExpressionComment, removeExpressionComment } from '~/queries/nod import { InsightEmptyState, InsightErrorState } from 'scenes/insights/EmptyStates' import { EventType } from '~/types' import { SavedQueries } from '~/queries/nodes/DataTable/SavedQueries' +import { HogQLQueryEditor } from '~/queries/nodes/HogQLQuery/HogQLQueryEditor' interface DataTableProps { query: DataTableNode @@ -79,6 +80,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele showSearch, showEventFilter, showPropertyFilter, + showHogQLEditor, showReload, showExport, showElapsedTime, @@ -293,7 +295,7 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele ].filter((column) => !query.hiddenColumns?.includes(column.dataIndex) && column.dataIndex !== '*') const setQuerySource = useCallback( - (source: EventsNode | EventsQuery | PersonsNode) => setQuery?.({ ...query, source }), + (source: EventsNode | EventsQuery | PersonsNode | HogQLQuery) => setQuery?.({ ...query, source }), [setQuery] ) @@ -338,7 +340,10 @@ export function DataTable({ query, setQuery, context }: DataTableProps): JSX.Ele return ( -
+
+ {showHogQLEditor && isHogQLQuery(query.source) ? ( + + ) : null} {showFirstRow && (
{firstRowLeft} diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index 359a1c136e434..f5165b33a6313 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -142,6 +142,7 @@ export const dataTableLogic = kea([ showColumnConfigurator: query.showColumnConfigurator ?? showIfFull, showSavedQueries: query.showSavedQueries ?? false, showEventsBufferWarning: query.showEventsBufferWarning ?? showIfFull, + showHogQLEditor: query.showHogQLEditor ?? showIfFull, allowSorting: query.allowSorting ?? true, }), } diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx new file mode 100644 index 0000000000000..2ee10bc896419 --- /dev/null +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -0,0 +1,55 @@ +import { useActions, useValues } from 'kea' +import { HogQLQuery } from '~/queries/schema' +import { useState } from 'react' +import { hogQLQueryEditorLogic } from './hogQLQueryEditorLogic' +import MonacoEditor from '@monaco-editor/react' +import { AutoSizer } from 'react-virtualized/dist/es/AutoSizer' +import { LemonButton } from 'lib/lemon-ui/LemonButton' + +export interface HogQLQueryEditorProps { + query: HogQLQuery + setQuery?: (query: HogQLQuery) => void +} + +let uniqueNode = 0 +export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { + const [key] = useState(() => uniqueNode++) + const hogQLQueryEditorLogicProps = { query: props.query, setQuery: props.setQuery, key } + const { queryInput } = useValues(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps)) + const { setQueryInput, saveQuery } = useActions(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps)) + + return ( +
+
+ + {({ height }) => ( + setQueryInput(v ?? '')} + height={height} + options={{ + minimap: { + enabled: false, + }, + wordWrap: 'on', + }} + /> + )} + +
+ + {!props.setQuery ? 'No permission to update' : 'Update'} + +
+ ) +} diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts new file mode 100644 index 0000000000000..474926ec9f1d3 --- /dev/null +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -0,0 +1,34 @@ +import { actions, kea, key, listeners, path, props, propsChanged, reducers } from 'kea' +import { HogQLQuery } from '~/queries/schema' + +import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' + +export interface HogQLQueryEditorLogicProps { + key: number + query: HogQLQuery + setQuery?: (query: HogQLQuery) => void +} + +export const hogQLQueryEditorLogic = kea([ + path(['queries', 'nodes', 'HogQLQuery', 'hogQLQueryEditorLogic']), + props({} as HogQLQueryEditorLogicProps), + key((props) => props.key), + propsChanged(({ actions, props }, oldProps) => { + if (props.query.query !== oldProps.query.query) { + actions.setQueryInput(props.query.query) + } + }), + actions({ + saveQuery: true, + setQueryInput: (queryInput: string) => ({ queryInput }), + }), + reducers(({ props }) => ({ + queryInput: [props.query.query, { setQueryInput: (_, { queryInput }) => queryInput }], + })), + listeners(({ actions, props, values }) => ({ + saveQuery: () => { + actions.setQueryInput(values.queryInput) + props.setQuery?.({ ...props.query, query: values.queryInput }) + }, + })), +]) diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index c36176a99a696..7ca8fafb769ec 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -229,6 +229,8 @@ export interface DataTableNode extends Node { showSearch?: boolean /** Include a property filter above the table */ showPropertyFilter?: boolean + /** Include a HogQL query editor above HogQL tables */ + showHogQLEditor?: boolean /** Show the kebab menu at the end of the row */ showActions?: boolean /** Show date range selector */ From d731524f6471ceadc8837a7dceb134025384562b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 11:36:47 +0100 Subject: [PATCH 057/142] hide query editor if opening a hogql table --- .../queries/nodes/DataTable/dataTableLogic.ts | 4 ++- frontend/src/scenes/query/QueryScene.tsx | 25 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts index f5165b33a6313..e4bf1edbf9c3d 100644 --- a/frontend/src/queries/nodes/DataTable/dataTableLogic.ts +++ b/frontend/src/queries/nodes/DataTable/dataTableLogic.ts @@ -138,7 +138,9 @@ export const dataTableLogic = kea([ showDateRange: query.showDateRange ?? showIfFull, showExport: query.showExport ?? showIfFull, showReload: query.showReload ?? showIfFull, - showElapsedTime: query.showElapsedTime ?? (flagQueryRunningTimeEnabled ? showIfFull : false), + showElapsedTime: + query.showElapsedTime ?? + (flagQueryRunningTimeEnabled || source.kind === NodeKind.HogQLQuery ? showIfFull : false), showColumnConfigurator: query.showColumnConfigurator ?? showIfFull, showSavedQueries: query.showSavedQueries ?? false, showEventsBufferWarning: query.showEventsBufferWarning ?? showIfFull, diff --git a/frontend/src/scenes/query/QueryScene.tsx b/frontend/src/scenes/query/QueryScene.tsx index 6c0337dd51fb5..fb44c06db3813 100644 --- a/frontend/src/scenes/query/QueryScene.tsx +++ b/frontend/src/scenes/query/QueryScene.tsx @@ -14,6 +14,19 @@ export function QueryScene(): JSX.Element { const { query } = useValues(querySceneLogic) const { setQuery } = useActions(querySceneLogic) + let showEditor = true + try { + const parsed = JSON.parse(query) + if ( + parsed && + parsed.kind == 'DataTableNode' && + parsed.source.kind == 'HogQLQuery' && + (parsed.full || parsed.showHogQLEditor) + ) { + showEditor = false + } + } catch (e) {} + return (
@@ -35,10 +48,14 @@ export function QueryScene(): JSX.Element { ))}
- -
- -
+ {showEditor ? ( + <> + +
+ +
+ + ) : null} setQuery(JSON.stringify(query, null, 2))} />
From 0759e0864fd62d06faeb76f164fa8113c2790bf0 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 11:03:00 +0000 Subject: [PATCH 058/142] Update snapshots --- .../api/test/__snapshots__/test_query.ambr | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 9b049fa990e6c..158a059e51a88 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -92,6 +92,30 @@ LIMIT 101 ' --- +# name: TestQuery.test_full_hogql_query + ' + /* user_id:0 request:_snapshot_ */ + SELECT event, + distinct_id, + replaceRegexpAll(JSONExtractRaw(properties, 'key'), '^"|"$', '') + FROM events + WHERE equals(team_id, 68) + ORDER BY timestamp ASC + LIMIT 1000 + ' +--- +# name: TestQuery.test_full_hogql_query_materialized + ' + /* user_id:0 request:_snapshot_ */ + SELECT event, + distinct_id, + mat_key + FROM events + WHERE equals(team_id, 69) + ORDER BY timestamp ASC + LIMIT 1000 + ' +--- # name: TestQuery.test_hogql_property_filter ' /* user_id:0 request:_snapshot_ */ From 5605a9440a1d56239670a97200034523ea6c140c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 11:30:43 +0000 Subject: [PATCH 059/142] Update snapshots --- .../__snapshots__/test_session_recording_list.ambr | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr b/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr index 1a633f0a9edbb..79070c7c9eaf0 100644 --- a/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr +++ b/posthog/queries/session_recordings/test/__snapshots__/test_session_recording_list.ambr @@ -483,14 +483,14 @@ any(session_recordings.duration) as duration, any(session_recordings.distinct_id) as distinct_id , countIf(event = '$pageview' - AND (equals("mat_$browser", 'Chrome') - AND equals("pmat_email", 'bla'))) as count_event_match_0 , + AND (equals(`mat_$browser`, 'Chrome') + AND equals(pmat_email, 'bla'))) as count_event_match_0 , groupUniqArrayIf(100)((events.timestamp, events.uuid, events.session_id, events.window_id), event = '$pageview' - AND (equals("mat_$browser", 'Chrome') - AND equals("pmat_email", 'bla'))) as matching_events_0 + AND (equals(`mat_$browser`, 'Chrome') + AND equals(pmat_email, 'bla'))) as matching_events_0 FROM (SELECT uuid, distinct_id, @@ -562,12 +562,12 @@ any(session_recordings.duration) as duration, any(session_recordings.distinct_id) as distinct_id , countIf(event = '$pageview' - AND (equals("mat_$browser", 'Firefox'))) as count_event_match_0 , + AND (equals(`mat_$browser`, 'Firefox'))) as count_event_match_0 , groupUniqArrayIf(100)((events.timestamp, events.uuid, events.session_id, events.window_id), event = '$pageview' - AND (equals("mat_$browser", 'Firefox'))) as matching_events_0 + AND (equals(`mat_$browser`, 'Firefox'))) as matching_events_0 FROM (SELECT uuid, distinct_id, From 1b02f0258fcd12717762e7e05cecfc78c020368b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 12:38:10 +0100 Subject: [PATCH 060/142] query into snapshot data --- posthog/hogql/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 7341258bb58bc..3bed8aa173c94 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -117,10 +117,10 @@ class SessionRecordingEvents(Table): distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") session_id: StringDatabaseField = StringDatabaseField(name="session_id") window_id: StringDatabaseField = StringDatabaseField(name="window_id") - snapshot_data: StringDatabaseField = StringDatabaseField(name="snapshot_data") + snapshot_data: StringJSONDatabaseField = StringJSONDatabaseField(name="snapshot_data") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") has_full_snapshot: BooleanDatabaseField = BooleanDatabaseField(name="has_full_snapshot") - events_summary: BooleanDatabaseField = BooleanDatabaseField(name="events_summary", array=True) + events_summary: StringJSONDatabaseField = StringJSONDatabaseField(name="events_summary", array=True) click_count: IntegerDatabaseField = IntegerDatabaseField(name="click_count") keypress_count: IntegerDatabaseField = IntegerDatabaseField(name="keypress_count") timestamps_summary: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamps_summary", array=True) From b59cf91f4c16c5109d50514a8e1b1ff70c8eb047 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 13:28:48 +0100 Subject: [PATCH 061/142] splash expander --- posthog/hogql/ast.py | 5 +++++ posthog/hogql/database.py | 14 +++++++------- posthog/hogql/parser.py | 6 ++++++ posthog/hogql/printer.py | 5 ++++- posthog/hogql/resolver.py | 2 +- posthog/hogql/transforms.py | 28 ++++++++++++++++++++++++++++ 6 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 posthog/hogql/transforms.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 2d55753b9d851..c1b0ad4d3f6e5 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -53,7 +53,12 @@ def has_child(self, name: str) -> bool: return self.table.has_field(name) def get_child(self, name: str) -> Symbol: + if name == "*": + return SplashSymbol(table=self) if self.has_child(name): + field = self.table.get_field(name) + if isinstance(field, Table): + return SplashSymbol(table=TableSymbol(table=field)) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 3bed8aa173c94..55e8a54812307 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict from pydantic import BaseModel, Extra @@ -48,17 +48,17 @@ def get_field(self, name: str) -> DatabaseField: def clickhouse_table(self): raise NotImplementedError("Table.clickhouse_table not overridden") - def get_splash(self) -> List[str]: - list: List[str] = [] - for field in self.__fields__.values(): + def get_splash(self) -> Dict[str, DatabaseField]: + splash: Dict[str, DatabaseField] = {} + for key, field in self.__fields__.items(): database_field = field.default if isinstance(database_field, DatabaseField): - list.append(database_field.name) + splash[key] = database_field elif isinstance(database_field, Table): - list.extend(database_field.get_splash()) + pass # ignore virtual tables for now else: raise ValueError(f"Unknown field type {type(database_field).__name__} for splash") - return list + return splash class PersonsTable(Table): diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index 5b7f4b9eff6fc..b7be7d161938a 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -296,6 +296,9 @@ def visitColumnExprList(self, ctx: HogQLParser.ColumnExprListContext): return [self.visit(c) for c in ctx.columnsExpr()] def visitColumnsExprAsterisk(self, ctx: HogQLParser.ColumnsExprAsteriskContext): + if ctx.tableIdentifier(): + table = self.visit(ctx.tableIdentifier()) + return ast.Field(chain=table + ["*"]) return ast.Field(chain=["*"]) def visitColumnsExprSubquery(self, ctx: HogQLParser.ColumnsExprSubqueryContext): @@ -500,6 +503,9 @@ def visitColumnExprFunction(self, ctx: HogQLParser.ColumnExprFunctionContext): return ast.Call(name=name, args=args) def visitColumnExprAsterisk(self, ctx: HogQLParser.ColumnExprAsteriskContext): + if ctx.tableIdentifier(): + table = self.visit(ctx.tableIdentifier()) + return ast.Field(chain=table + ["*"]) return ast.Field(chain=["*"]) def visitColumnArgList(self, ctx: HogQLParser.ColumnArgListContext): diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 5f27ff8ce3ba5..c0e20b4619e68 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -8,6 +8,7 @@ from posthog.hogql.database import Table, database from posthog.hogql.print_string import print_clickhouse_identifier, print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols +from posthog.hogql.transforms import expand_splashes from posthog.hogql.visitor import Visitor from posthog.models.property import PropertyName, TableColumn @@ -40,6 +41,8 @@ def print_ast( # modify the cloned tree as needed if dialect == "clickhouse": + expand_splashes(node) + # TODO: add team_id checks (currently done in the printer) # TODO: add joins to person and group tables pass @@ -475,7 +478,7 @@ def visit_splash_symbol(self, symbol: ast.SplashSymbol): prefix = ( f"{self._print_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" ) - return f"tuple({', '.join(f'{prefix}{self._print_identifier(field)}' for field in splash_fields)})" + return f"tuple({', '.join(f'{prefix}{self._print_identifier(field.name)}' for chain, field in splash_fields.items())})" def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index e08e6215a64ed..965b149864d2c 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -165,7 +165,7 @@ def visit_field(self, node): if table_count == 0: raise ResolverException("Cannot use '*' when there are no tables in the query") if table_count > 1: - raise ResolverException("Cannot use '*' when there are multiple tables in the query") + raise ResolverException("Cannot use '*' without table name when there are multiple tables in the query") table = scope.anonymous_tables[0] if len(scope.anonymous_tables) > 0 else list(scope.tables.values())[0] symbol = ast.SplashSymbol(table=table) diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py new file mode 100644 index 0000000000000..0cc05d1f8019e --- /dev/null +++ b/posthog/hogql/transforms.py @@ -0,0 +1,28 @@ +from typing import List + +from posthog.hogql import ast +from posthog.hogql.visitor import TraversingVisitor + + +def expand_splashes(node: ast.Expr): + SplashExpander().visit(node) + + +class SplashExpander(TraversingVisitor): + def visit_select_query(self, node: ast.SelectQuery): + columns: List[ast.Expr] = [] + for column in node.select: + if isinstance(column.symbol, ast.SplashSymbol): + splash = column.symbol + table = splash.table + while isinstance(table, ast.TableAliasSymbol): + table = table.table + if isinstance(table, ast.TableSymbol): + database_fields = table.table.get_splash() + for key in database_fields.keys(): + columns.append(ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=splash.table))) + else: + raise ValueError("Can't expand splash (*) on subquery") + else: + columns.append(column) + node.select = columns From 5cf6e77bd3f188d1baaed934ea44ec7595982751 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 13:31:19 +0100 Subject: [PATCH 062/142] code quality issues --- frontend/src/queries/schema.json | 4 ++++ posthog/api/query.py | 8 ++++---- posthog/api/test/test_query.py | 2 +- posthog/schema.py | 1 + 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 837732a4ce7ec..3c61bf014ab6d 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1247,6 +1247,10 @@ "description": "Show the export button", "type": "boolean" }, + "showHogQLEditor": { + "description": "Include a HogQL query editor above HogQL tables", + "type": "boolean" + }, "showPropertyFilter": { "description": "Include a property filter above the table", "type": "boolean" diff --git a/posthog/api/query.py b/posthog/api/query.py index 289b1d4d366b2..80a2aa4c664cb 100644 --- a/posthog/api/query.py +++ b/posthog/api/query.py @@ -45,12 +45,12 @@ def _process_query(self, team: Team, query_json: Dict) -> JsonResponse: try: query_kind = query_json.get("kind") if query_kind == "EventsQuery": - query = EventsQuery.parse_obj(query_json) - response = run_events_query(query=query, team=team) + events_query = EventsQuery.parse_obj(query_json) + response = run_events_query(query=events_query, team=team) return self._response_to_json_response(response) elif query_kind == "HogQLQuery": - query = HogQLQuery.parse_obj(query_json) - response = execute_hogql_query(query=query.query, team=team) + hogql_query = HogQLQuery.parse_obj(query_json) + response = execute_hogql_query(query=hogql_query.query, team=team) return self._response_to_json_response(response) else: raise ValidationError("Unsupported query kind: %s" % query_kind) diff --git a/posthog/api/test/test_query.py b/posthog/api/test/test_query.py index 8ac341658f368..4b164743f5580 100644 --- a/posthog/api/test/test_query.py +++ b/posthog/api/test/test_query.py @@ -291,7 +291,7 @@ def test_full_hogql_query(self): api_response = self.client.post(f"/api/projects/{self.team.id}/query/", query.dict()).json() query.response = HogQLQueryResponse.parse_obj(api_response) - self.assertEqual(len(query.response.results), 4) + self.assertEqual(query.response.results and len(query.response.results), 4) self.assertEqual( query.response.results, [ diff --git a/posthog/schema.py b/posthog/schema.py index f6a89c5905cd7..72e9da3f90757 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -845,6 +845,7 @@ class Config: None, description="Show warning about live events being buffered max 60 sec (default: false)" ) showExport: Optional[bool] = Field(None, description="Show the export button") + showHogQLEditor: Optional[bool] = Field(None, description="Include a HogQL query editor above HogQL tables") showPropertyFilter: Optional[bool] = Field(None, description="Include a property filter above the table") showReload: Optional[bool] = Field(None, description="Show a reload button") showSavedQueries: Optional[bool] = Field(None, description="Shows a list of saved queries") From 44ff1a455ab929443e4c1472276b0241268fbc85 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 16 Feb 2023 13:37:23 +0100 Subject: [PATCH 063/142] error if unexpected splash --- posthog/hogql/printer.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index c0e20b4619e68..4a49c6a9496e0 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -469,16 +469,7 @@ def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return self._print_identifier(symbol.name) def visit_splash_symbol(self, symbol: ast.SplashSymbol): - table = symbol.table - while isinstance(table, ast.TableAliasSymbol): - table = table.table - if not isinstance(table, ast.TableSymbol): - raise ValueError(f"Unknown SplashSymbol table type: {type(table).__name__}") - splash_fields = table.table.get_splash() - prefix = ( - f"{self._print_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" - ) - return f"tuple({', '.join(f'{prefix}{self._print_identifier(field.name)}' for chain, field in splash_fields.items())})" + raise ValueError("Unexpected splash (*). It's only allowed in a SELECT column.") def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") From fdf186132abff17719036c0cf27ca925059684d9 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 17 Feb 2023 00:13:26 +0100 Subject: [PATCH 064/142] bandage --- posthog/models/event/query_event_list.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index 878bf00e4ed32..f4ebde9d58e38 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -209,6 +209,8 @@ def run_events_query( select = ["*"] for expr in select: + if expr == "*": + expr = f"tuple({', '.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" hogql_context.found_aggregation = False clickhouse_sql = translate_hogql(expr, hogql_context) select_columns.append(clickhouse_sql) @@ -293,14 +295,6 @@ def run_events_query( def convert_star_select_to_dict(select: Tuple[Any]) -> Dict[str, Any]: new_result = dict(zip(SELECT_STAR_FROM_EVENTS_FIELDS, select)) new_result["properties"] = json.loads(new_result["properties"]) - new_result["person"] = { - "id": new_result["person.id"], - "created_at": new_result["person.created_at"], - "properties": json.loads(new_result["person.properties"]), - } - new_result.pop("person.id") - new_result.pop("person.created_at") - new_result.pop("person.properties") if new_result["elements_chain"]: new_result["elements"] = ElementSerializer(chain_to_elements(new_result["elements_chain"]), many=True).data return new_result From a0e0dce5049bb453539026697221781778ff1f06 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 17 Feb 2023 00:14:45 +0100 Subject: [PATCH 065/142] lazy table joins --- posthog/hogql/ast.py | 43 +++++++++++++++-- posthog/hogql/constants.py | 3 -- posthog/hogql/database.py | 64 ++++++++++++++++++++++++-- posthog/hogql/printer.py | 13 +++--- posthog/hogql/query.py | 11 ++++- posthog/hogql/test/test_query.py | 75 ++++++++++++++++++++++++++++-- posthog/hogql/transforms.py | 79 +++++++++++++++++++++++++++++++- posthog/hogql/visitor.py | 49 ++++++++++++++++++-- 8 files changed, 311 insertions(+), 26 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index c1b0ad4d3f6e5..f4134b5b18e76 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import DatabaseField, StringJSONDatabaseField, Table +from posthog.hogql.database import DatabaseField, JoinedTable, StringJSONDatabaseField, Table # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -59,6 +59,32 @@ def get_child(self, name: str) -> Symbol: field = self.table.get_field(name) if isinstance(field, Table): return SplashSymbol(table=TableSymbol(table=field)) + if isinstance(field, JoinedTable): + return LazyTableSymbol(table=self, field=name, joined_table=field) + return FieldSymbol(name=name, table=self) + raise ValueError(f"Field not found: {name}") + + +class LazyTableSymbol(Symbol): + table: TableSymbol + field: str + joined_table: JoinedTable + + def __init__(self, **data): + super().__init__(**data) + + def has_child(self, name: str) -> bool: + return self.joined_table.table.has_field(name) + + def get_child(self, name: str) -> Symbol: + if name == "*": + return SplashSymbol(table=self) + if self.has_child(name): + field = self.joined_table.table.get_field(name) + if isinstance(field, Table): + return SplashSymbol(table=TableSymbol(table=field)) + if isinstance(field, JoinedTable): + return LazyTableSymbol(table=self, field=name, joined_table=field) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") @@ -83,11 +109,20 @@ class SelectQuerySymbol(Symbol): columns: Dict[str, Symbol] = PydanticField(default_factory=dict) # all from and join, tables and subqueries with aliases tables: Dict[ - str, Union[TableSymbol, TableAliasSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"] + str, Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"] ] = PydanticField(default_factory=dict) # all from and join subqueries without aliases anonymous_tables: List["SelectQuerySymbol"] = PydanticField(default_factory=list) + def key_for_table( + self, + table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"], + ) -> Optional[str]: + for key, value in self.tables.items(): + if value == table: + return key + return None + def get_child(self, name: str) -> Symbol: if name in self.columns: return FieldSymbol(name=name, table=self) @@ -124,12 +159,12 @@ class ConstantSymbol(Symbol): class SplashSymbol(Symbol): - table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldSymbol(Symbol): name: str - table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] def resolve_database_field(self) -> Optional[Union[DatabaseField, Table]]: table_symbol = self.table diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 92c88a8c65df4..27e180bec6574 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -110,9 +110,6 @@ "distinct_id", "elements_chain", "created_at", - "person.id", - "person.created_at", - "person.properties", ] # Never return more rows than this in top level HogQL SELECT statements diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 55e8a54812307..7afb1d673334c 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Any, Callable, Dict, List from pydantic import BaseModel, Extra @@ -52,9 +52,11 @@ def get_splash(self) -> Dict[str, DatabaseField]: splash: Dict[str, DatabaseField] = {} for key, field in self.__fields__.items(): database_field = field.default - if isinstance(database_field, DatabaseField): + if key == "team_id": + pass # skip team_id + elif isinstance(database_field, DatabaseField): splash[key] = database_field - elif isinstance(database_field, Table): + elif isinstance(database_field, Table) or isinstance(database_field, JoinedTable): pass # ignore virtual tables for now else: raise ValueError(f"Unknown field type {type(database_field).__name__} for splash") @@ -81,6 +83,13 @@ class PersonDistinctIdTable(Table): is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") + def get_splash(self) -> Dict[str, DatabaseField]: + splash: Dict[str, DatabaseField] = {} + for key, value in super().get_splash().items(): + if key != "is_deleted" and key != "version": + splash[key] = value + return splash + def clickhouse_table(self): return "person_distinct_id2" @@ -95,6 +104,53 @@ def clickhouse_table(self): return "events" +class JoinedTable(BaseModel): + class Config: + extra = Extra.forbid + + join_function: Callable[[str, str, List[str]], Any] + table: Table + + +def join_events_to_max_person_distinct_id(events_alias: str, pdi_alias: str, requested_fields: List[str]): + from posthog.hogql import ast + + if not requested_fields: + requested_fields = ["person_id"] + + # contains the list of fields we will select from this table + fields_to_select: List[ast.Expr] = [] + + max_version: Callable[[ast.Expr], ast.Expr] = lambda field: ast.Call( + name="argMax", args=[field, ast.Field(chain=["version"])] + ) + for field in requested_fields: + if field != "distinct_id": + fields_to_select.append(ast.Alias(alias=field, expr=max_version(ast.Field(chain=[field])))) + + 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=max_version(ast.Field(chain=["is_deleted"])), + right=ast.Constant(value=0), + ), + ), + alias=pdi_alias, + constraint=ast.CompareOperation( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=[events_alias, "distinct_id"]), + right=ast.Field(chain=[pdi_alias, "distinct_id"]), + ), + ) + + class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") event: StringDatabaseField = StringDatabaseField(name="event") @@ -106,6 +162,8 @@ class EventsTable(Table): created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") person: EventsPersonSubTable = EventsPersonSubTable() + pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_events_to_max_person_distinct_id) + def clickhouse_table(self): return "events" diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4a49c6a9496e0..fc2edd8239794 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -8,7 +8,7 @@ from posthog.hogql.database import Table, database from posthog.hogql.print_string import print_clickhouse_identifier, print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols -from posthog.hogql.transforms import expand_splashes +from posthog.hogql.transforms import expand_splashes, resolve_lazy_tables from posthog.hogql.visitor import Visitor from posthog.models.property import PropertyName, TableColumn @@ -31,7 +31,7 @@ def print_ast( node: ast.Expr, context: HogQLContext, dialect: Literal["hogql", "clickhouse"], - stack: Optional[List[ast.Expr]] = None, + stack: Optional[List[ast.SelectQuery]] = None, ) -> str: """Print an AST into a string. Does not modify the node.""" symbol = stack[-1].symbol if stack else None @@ -42,10 +42,8 @@ def print_ast( # modify the cloned tree as needed if dialect == "clickhouse": expand_splashes(node) - + resolve_lazy_tables(node) # TODO: add team_id checks (currently done in the printer) - # TODO: add joins to person and group tables - pass return Printer(context=context, dialect=dialect, stack=stack or []).visit(node) @@ -469,7 +467,10 @@ def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return self._print_identifier(symbol.name) def visit_splash_symbol(self, symbol: ast.SplashSymbol): - raise ValueError("Unexpected splash (*). It's only allowed in a SELECT column.") + raise ValueError("Unexpected ast.SplashSymbol. Make sure SplashExpander has run on the AST.") + + def visit_lazy_table_symbol(self, symbol: ast.LazyTableSymbol): + raise ValueError("Unexpected ast.LazyTableSymbol. Make sure LazyTableResolver has run on the AST.") def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") diff --git a/posthog/hogql/query.py b/posthog/hogql/query.py index 45251b69a610b..8f8b504a11c05 100644 --- a/posthog/hogql/query.py +++ b/posthog/hogql/query.py @@ -8,6 +8,7 @@ from posthog.hogql.parser import parse_select from posthog.hogql.placeholders import assert_no_placeholders, replace_placeholders from posthog.hogql.printer import print_ast +from posthog.hogql.visitor import clone_expr from posthog.models import Team from posthog.queries.insight import insight_sync_execute @@ -45,9 +46,13 @@ def execute_hogql_query( if select_query.limit is None: select_query.limit = ast.Constant(value=1000) + # Make a copy for hogql printing later. we don't want it to contain joined SQL tables for example + select_query_hogql = clone_expr(select_query) + hogql_context = HogQLContext(select_team_id=team.pk, using_person_on_events=team.person_on_events_querying_enabled) clickhouse = print_ast(select_query, hogql_context, "clickhouse") - hogql = print_ast(select_query, hogql_context, "hogql") + + hogql = print_ast(select_query_hogql, hogql_context, "hogql") results, types = insight_sync_execute( clickhouse, @@ -61,7 +66,9 @@ def execute_hogql_query( if isinstance(node, ast.Alias): print_columns.append(node.alias) else: - print_columns.append(print_ast(node=node, context=hogql_context, dialect="hogql", stack=[select_query])) + print_columns.append( + print_ast(node=node, context=hogql_context, dialect="hogql", stack=[select_query_hogql]) + ) return HogQLQueryResponse( query=query, diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 25c9256bad1fd..07988473471de 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -131,19 +131,88 @@ def test_query_joins_simple(self): self.assertEqual(response.results[0][2], "bla") self.assertEqual(response.results[0][4], "tim@posthog.com") - def test_query_joins(self): + def test_query_joins_pdi(self): with freeze_time("2020-01-10"): self._create_random_events() response = execute_hogql_query( - """select event, timestamp, pdi.person_id from events e INNER JOIN ( + """ + SELECT event, timestamp, pdi.person_id from events e + INNER JOIN ( + SELECT distinct_id, + argMax(person_id, version) as person_id + FROM person_distinct_ids + GROUP BY distinct_id + HAVING argMax(is_deleted, version) = 0 + ) AS pdi + ON e.distinct_id = pdi.distinct_id + """, + self.team, + ) + + self.assertEqual( + response.clickhouse, + f"SELECT e.event, e.timestamp, pdi.person_id FROM events AS e INNER JOIN (SELECT distinct_id, argMax(person_distinct_id2.person_id, version) AS person_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) WHERE equals(e.team_id, {self.team.id}) LIMIT 1000", + ) + 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_id2 GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) LIMIT 1000", + ) + self.assertTrue(len(response.results) > 0) + + def test_query_joins_pdi_automatic(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + """ + SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10 + """, + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT event, timestamp, events_pdi.distinct_id, events_pdi.person_id FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, 1) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS events_pdi ON equals(events.distinct_id, events_pdi.distinct_id) WHERE equals(team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events INNER JOIN (SELECT argMax(person_id, version) AS person_id, distinct_id FROM person_distinct_id2 GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS events_pdi ON equals(events.distinct_id, events_pdi.distinct_id) LIMIT 10", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][4], "00000000-0000-4000-8000-000000000001") + + def test_query_joins_person(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + """ + SELECT event, timestamp, pdi.person_id from events e + INNER JOIN ( SELECT distinct_id, argMax(person_id, version) as person_id FROM person_distinct_ids GROUP BY distinct_id HAVING argMax(is_deleted, version) = 0 ) AS pdi - ON e.distinct_id = pdi.distinct_id""", + ON e.distinct_id = pdi.distinct_id + INNER JOIN ( + SELECT id + FROM person + WHERE team_id = 1 + AND id IN ( + SELECT id + FROM person + WHERE team_id = 1 + AND (has(['Chrome'], replaceRegexpAll(JSONExtractRaw(person.properties, '$browser'), '^"|"$', ''))) + ) + GROUP BY id + HAVING max(is_deleted) = 0 + AND (has(['Chrome'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), '$browser'), '^"|"$', ''))) + ) person + ON person.id = pdi.person_id + """, self.team, ) diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index 0cc05d1f8019e..f22e1134d8db3 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -1,6 +1,8 @@ -from typing import List +import dataclasses +from typing import Callable, Dict, List, Set, Union from posthog.hogql import ast +from posthog.hogql.resolver import resolve_symbols from posthog.hogql.visitor import TraversingVisitor @@ -21,8 +23,83 @@ def visit_select_query(self, node: ast.SelectQuery): database_fields = table.table.get_splash() for key in database_fields.keys(): columns.append(ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=splash.table))) + elif isinstance(table, ast.LazyTableSymbol): + database_fields = table.joined_table.table.get_splash() + for key in database_fields.keys(): + columns.append(ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=splash.table))) else: raise ValueError("Can't expand splash (*) on subquery") else: columns.append(column) node.select = columns + + +def resolve_lazy_tables(node: ast.Expr): + LazyTableResolver().visit(node) + + +class LazyTableResolver(TraversingVisitor): + def __init__(self): + super().__init__() + self.fields: List[List[Union[ast.FieldSymbol]]] = [] + + def visit_select_query(self, node: ast.SelectQuery): + if not node.symbol: + raise ValueError("Select query must have a symbol") + + # Collects each `ast.Field` with `ast.LazyTableSymbol` + lazy_fields: List[Union[ast.FieldSymbol]] = [] + self.fields.append(lazy_fields) + + super().visit_select_query(node) + + last_join = node.select_from + while last_join.next_join is not None: + last_join = last_join.next_join + + @dataclasses.dataclass + class LocalScope: + fields_accessed: Set[str] + join_function: Callable[[str, str, List[str]], ast.JoinExpr] + parent_key: str + table_alias: str + + new_tables: Dict[str, LocalScope] = {} + + for field in lazy_fields: + if not isinstance(field.table, ast.LazyTableSymbol): + raise ValueError("Should not be reachable.") + parent_key = node.symbol.key_for_table(field.table.table) + if parent_key is None: + raise ValueError("Should not be reachable.") + table_alias = f"{parent_key}_{field.table.field}" + if not new_tables.get(table_alias): + new_tables[table_alias] = LocalScope( + fields_accessed=set(), + join_function=field.table.joined_table.join_function, + parent_key=parent_key, + table_alias=table_alias, + ) + new_tables[table_alias].fields_accessed.add(field.name) + + for table_alias, scope in new_tables.items(): + next_join = scope.join_function(scope.parent_key, scope.table_alias, list(scope.fields_accessed)) + resolve_symbols(next_join, node.symbol) + node.symbol.tables[table_alias] = next_join.symbol # type: ignore + + last_join.next_join = next_join + while last_join.next_join is not None: + last_join = last_join.next_join + + for field in lazy_fields: + parent_key = node.symbol.key_for_table(field.table.table) + table_alias = f"{parent_key}_{field.table.field}" + field.table = node.symbol.tables[table_alias] + + self.fields.pop() + + def visit_field_symbol(self, node: ast.FieldSymbol): + if isinstance(node.table, ast.LazyTableSymbol): + if len(self.fields) == 0: + raise ValueError("Can't access a lazy field when not in a SelectQuery context") + self.fields[-1].append(node) diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index e6d94d2ed9ad4..01de7a3c85377 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -45,13 +45,13 @@ def visit_order_expr(self, node: ast.OrderExpr): self.visit(node.expr) def visit_constant(self, node: ast.Constant): - pass + self.visit(node.symbol) def visit_field(self, node: ast.Field): - pass + self.visit(node.symbol) def visit_placeholder(self, node: ast.Placeholder): - pass + self.visit(node.symbol) def visit_call(self, node: ast.Call): for expr in node.args: @@ -78,9 +78,50 @@ def visit_select_query(self, node: ast.SelectQuery): self.visit(node.limit), self.visit(node.offset), + def visit_field_alias_symbol(self, node: ast.FieldAliasSymbol): + self.visit(node.symbol) + + def visit_field_symbol(self, node: ast.FieldSymbol): + self.visit(node.table) + + def visit_select_query_symbol(self, node: ast.SelectQuerySymbol): + for expr in node.tables.values(): + self.visit(expr) + for expr in node.anonymous_tables: + self.visit(expr) + for expr in node.aliases.values(): + self.visit(expr) + for expr in node.columns.values(): + self.visit(expr) + + def visit_table_symbol(self, node: ast.TableSymbol): + pass + + def visit_lazy_table_symbol(self, node: ast.LazyTableSymbol): + self.visit(node.table) + + def visit_table_alias_symbol(self, node: ast.TableAliasSymbol): + self.visit(node.table) + + def visit_select_query_alias_symbol(self, node: ast.SelectQueryAliasSymbol): + self.visit(node.symbol) + + def visit_splash_symbol(self, node: ast.SplashSymbol): + self.visit(node.table) + + def visit_call_symbol(self, node: ast.CallSymbol): + for expr in node.args: + self.visit(expr) + + def visit_constant_symbol(self, node: ast.ConstantSymbol): + pass + + def visit_property_symbol(self, node: ast.PropertySymbol): + self.visit(node.parent) + class CloningVisitor(Visitor): - """Visitor that traverses and clones the AST tree""" + """Visitor that traverses and clones the AST tree. Clears symbols.""" def visit_expr(self, node: ast.Expr): raise ValueError("Can not visit generic Expr node") From 750f165948f70cf2418fba1638a1d18f03aeb6bc Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 17 Feb 2023 00:22:40 +0100 Subject: [PATCH 066/142] test --- posthog/hogql/database.py | 16 ++++++++-------- posthog/hogql/test/test_query.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 7afb1d673334c..0fefa314e6691 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -63,6 +63,14 @@ def get_splash(self) -> Dict[str, DatabaseField]: return splash +class JoinedTable(BaseModel): + class Config: + extra = Extra.forbid + + join_function: Callable[[str, str, List[str]], Any] + table: Table + + class PersonsTable(Table): id: StringDatabaseField = StringDatabaseField(name="id") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") @@ -104,14 +112,6 @@ def clickhouse_table(self): return "events" -class JoinedTable(BaseModel): - class Config: - extra = Extra.forbid - - join_function: Callable[[str, str, List[str]], Any] - table: Table - - def join_events_to_max_person_distinct_id(events_alias: str, pdi_alias: str, requested_fields: List[str]): from posthog.hogql import ast diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 07988473471de..4ca237bf61b7e 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -176,7 +176,7 @@ def test_query_joins_pdi_automatic(self): ) self.assertEqual( response.hogql, - "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events INNER JOIN (SELECT argMax(person_id, version) AS person_id, distinct_id FROM person_distinct_id2 GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS events_pdi ON equals(events.distinct_id, events_pdi.distinct_id) LIMIT 10", + "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10", ) self.assertEqual(response.results[0][0], "random event") self.assertEqual(response.results[0][2], "bla") From aee30aea27abeae13a08c7a42129e8b7f4054b7a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 17 Feb 2023 00:28:46 +0100 Subject: [PATCH 067/142] pdi for session_recording_events --- posthog/hogql/database.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 0fefa314e6691..5cc2294e51a52 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -112,7 +112,7 @@ def clickhouse_table(self): return "events" -def join_events_to_max_person_distinct_id(events_alias: str, pdi_alias: str, requested_fields: List[str]): +def join_with_max_person_distinct_id_table(base_table_alias: str, pdi_alias: str, requested_fields: List[str]): from posthog.hogql import ast if not requested_fields: @@ -145,7 +145,7 @@ def join_events_to_max_person_distinct_id(events_alias: str, pdi_alias: str, req alias=pdi_alias, constraint=ast.CompareOperation( op=ast.CompareOperationType.Eq, - left=ast.Field(chain=[events_alias, "distinct_id"]), + left=ast.Field(chain=[base_table_alias, "distinct_id"]), right=ast.Field(chain=[pdi_alias, "distinct_id"]), ), ) @@ -162,7 +162,7 @@ class EventsTable(Table): created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") person: EventsPersonSubTable = EventsPersonSubTable() - pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_events_to_max_person_distinct_id) + pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table) def clickhouse_table(self): return "events" @@ -186,6 +186,8 @@ class SessionRecordingEvents(Table): last_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="last_event_timestamp") urls: StringDatabaseField = StringDatabaseField(name="urls", array=True) + pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table) + def clickhouse_table(self): return "session_recording_events" From b8f1793e9d12dddecc4918c981167fd1382d6d86 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Feb 2023 23:34:36 +0000 Subject: [PATCH 068/142] Update snapshots --- posthog/api/test/__snapshots__/test_query.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 158a059e51a88..34702611c2ded 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -348,12 +348,12 @@ # name: TestQuery.test_select_hogql_expressions.1 ' /* user_id:0 request:_snapshot_ */ - SELECT tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at, person_id, person_created_at, person_properties), + SELECT tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at), event FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' - ORDER BY tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at, person_id, person_created_at, person_properties) ASC + ORDER BY tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at) ASC LIMIT 101 ' --- From 5d015fdd0e84e71344cc7476b7e6acee660d57e2 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 19 Feb 2023 15:13:52 +0100 Subject: [PATCH 069/142] replace old fake table joins with lazy table joins - resolver makes symbols correctly --- posthog/hogql/ast.py | 43 ++--- posthog/hogql/database.py | 70 +++++--- posthog/hogql/test/test_resolver.py | 251 ++++++++++++++++++++++++++++ 3 files changed, 323 insertions(+), 41 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index f4134b5b18e76..7921554c7b6c2 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -57,49 +57,51 @@ def get_child(self, name: str) -> Symbol: return SplashSymbol(table=self) if self.has_child(name): field = self.table.get_field(name) - if isinstance(field, Table): - return SplashSymbol(table=TableSymbol(table=field)) if isinstance(field, JoinedTable): return LazyTableSymbol(table=self, field=name, joined_table=field) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") -class LazyTableSymbol(Symbol): +class TableAliasSymbol(Symbol): + name: str table: TableSymbol - field: str - joined_table: JoinedTable - - def __init__(self, **data): - super().__init__(**data) def has_child(self, name: str) -> bool: - return self.joined_table.table.has_field(name) + return self.table.has_child(name) def get_child(self, name: str) -> Symbol: if name == "*": return SplashSymbol(table=self) if self.has_child(name): - field = self.joined_table.table.get_field(name) - if isinstance(field, Table): - return SplashSymbol(table=TableSymbol(table=field)) + table: Union[TableSymbol, TableAliasSymbol] = self + while isinstance(table, TableAliasSymbol): + table = table.table + field = table.table.get_field(name) + if isinstance(field, JoinedTable): return LazyTableSymbol(table=self, field=name, joined_table=field) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") -class TableAliasSymbol(Symbol): - name: str - table: TableSymbol +class LazyTableSymbol(Symbol): + table: Union[TableSymbol, TableAliasSymbol, "LazyTableSymbol"] + field: str + joined_table: JoinedTable def has_child(self, name: str) -> bool: - return self.table.has_child(name) + return self.joined_table.table.has_field(name) def get_child(self, name: str) -> Symbol: + if name == "*": + return SplashSymbol(table=self) if self.has_child(name): + field = self.joined_table.table.get_field(name) + if isinstance(field, JoinedTable): + return LazyTableSymbol(table=self, field=name, joined_table=field) return FieldSymbol(name=name, table=self) - return self.table.get_child(name) + raise ValueError(f"Field not found: {name}") class SelectQuerySymbol(Symbol): @@ -139,13 +141,12 @@ class SelectQueryAliasSymbol(Symbol): def get_child(self, name: str) -> Symbol: if self.symbol.has_child(name): return FieldSymbol(name=name, table=self) - raise ValueError(f"Field not found: {name}") + raise ValueError(f"Field {name} not found on query with alias {self.name}") def has_child(self, name: str) -> bool: return self.symbol.has_child(name) -SelectQuerySymbol.update_forward_refs(SelectQuerySymbol=SelectQuerySymbol) SelectQuerySymbol.update_forward_refs(SelectQueryAliasSymbol=SelectQueryAliasSymbol) @@ -178,8 +179,8 @@ def get_child(self, name: str) -> Symbol: database_field = self.resolve_database_field() if database_field is None: raise ValueError(f'Can not access property "{name}" on field "{self.name}".') - if isinstance(database_field, Table): - return FieldSymbol(name=name, table=TableSymbol(table=database_field)) + if isinstance(database_field, JoinedTable): + return FieldSymbol(name=name, table=LazyTableSymbol(table=self, field=name, joined_table=database_field)) if isinstance(database_field, StringJSONDatabaseField): return PropertySymbol(name=name, parent=self) raise ValueError( diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 5cc2294e51a52..b92fa4ececa40 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Optional from pydantic import BaseModel, Extra @@ -10,7 +10,7 @@ class Config: extra = Extra.forbid name: str - array: bool = False + array: Optional[bool] class IntegerDatabaseField(DatabaseField): @@ -84,6 +84,45 @@ def clickhouse_table(self): return "person" +def join_with_persons_table(from_table: str, to_table: str, requested_fields: List[str]): + from posthog.hogql import ast + + if not requested_fields: + raise ValueError("No fields requested from persons table. Why are we joining it?") + + # 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"])] + ) + for field in requested_fields: + if field != "id": + fields_to_select.append(ast.Alias(alias=field, expr=argmax_version(ast.Field(chain=[field])))) + + 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"])), + group_by=[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( + op=ast.CompareOperationType.Eq, + left=ast.Field(chain=[from_table, "person_id"]), + right=ast.Field(chain=[to_table, "id"]), + ), + ) + + class PersonDistinctIdTable(Table): team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") @@ -91,6 +130,8 @@ class PersonDistinctIdTable(Table): is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") + person: JoinedTable = JoinedTable(table=PersonsTable(), join_function=join_with_persons_table) + def get_splash(self) -> Dict[str, DatabaseField]: splash: Dict[str, DatabaseField] = {} for key, value in super().get_splash().items(): @@ -102,17 +143,7 @@ def clickhouse_table(self): return "person_distinct_id2" -class EventsPersonSubTable(Table): - id: StringDatabaseField = StringDatabaseField(name="person_id") - created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="person_created_at") - properties: StringJSONDatabaseField = StringJSONDatabaseField(name="person_properties") - - def clickhouse_table(self): - # This is a bit of a hack to make sure person.properties.x works - return "events" - - -def join_with_max_person_distinct_id_table(base_table_alias: str, pdi_alias: str, requested_fields: List[str]): +def join_with_max_person_distinct_id_table(from_table: str, to_table: str, requested_fields: List[str]): from posthog.hogql import ast if not requested_fields: @@ -121,12 +152,12 @@ def join_with_max_person_distinct_id_table(base_table_alias: str, pdi_alias: str # contains the list of fields we will select from this table fields_to_select: List[ast.Expr] = [] - max_version: Callable[[ast.Expr], ast.Expr] = lambda field: ast.Call( + argmax_version: Callable[[ast.Expr], ast.Expr] = lambda field: ast.Call( name="argMax", args=[field, ast.Field(chain=["version"])] ) for field in requested_fields: if field != "distinct_id": - fields_to_select.append(ast.Alias(alias=field, expr=max_version(ast.Field(chain=[field])))) + fields_to_select.append(ast.Alias(alias=field, expr=argmax_version(ast.Field(chain=[field])))) distinct_id = ast.Field(chain=["distinct_id"]) @@ -138,15 +169,15 @@ def join_with_max_person_distinct_id_table(base_table_alias: str, pdi_alias: str group_by=[distinct_id], having=ast.CompareOperation( op=ast.CompareOperationType.Eq, - left=max_version(ast.Field(chain=["is_deleted"])), + left=argmax_version(ast.Field(chain=["is_deleted"])), right=ast.Constant(value=0), ), ), - alias=pdi_alias, + alias=to_table, constraint=ast.CompareOperation( op=ast.CompareOperationType.Eq, - left=ast.Field(chain=[base_table_alias, "distinct_id"]), - right=ast.Field(chain=[pdi_alias, "distinct_id"]), + left=ast.Field(chain=[from_table, "distinct_id"]), + right=ast.Field(chain=[to_table, "distinct_id"]), ), ) @@ -160,7 +191,6 @@ class EventsTable(Table): distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - person: EventsPersonSubTable = EventsPersonSubTable() pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 8e4fd16cf095a..092bcb3570597 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -235,6 +235,257 @@ def test_resolve_errors(self): resolve_symbols(parse_select(query)) self.assertIn("Unable to resolve field:", str(e.exception)) + def test_resolve_lazy_pdi_person_table(self): + expr = parse_select("select distinct_id, person.id from person_distinct_ids") + resolve_symbols(expr) + pdi_table_symbol = ast.TableSymbol(table=database.person_distinct_ids) + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["distinct_id"], + symbol=ast.FieldSymbol(name="distinct_id", table=pdi_table_symbol), + ), + ast.Field( + chain=["person", "id"], + symbol=ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=pdi_table_symbol, field="person", joined_table=database.person_distinct_ids.person + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["person_distinct_ids"], symbol=pdi_table_symbol), + symbol=pdi_table_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "distinct_id": ast.FieldSymbol(name="distinct_id", table=pdi_table_symbol), + "id": ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=pdi_table_symbol, + joined_table=database.person_distinct_ids.person, + field="person", + ), + ), + }, + tables={"person_distinct_ids": pdi_table_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + + def test_resolve_lazy_events_pdi_table(self): + expr = parse_select("select event, pdi.person_id from events") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_symbol), + ), + ast.Field( + chain=["pdi", "person_id"], + symbol=ast.FieldSymbol( + name="person_id", + table=ast.LazyTableSymbol( + table=events_table_symbol, field="pdi", joined_table=database.events.pdi + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + symbol=events_table_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_symbol), + "person_id": ast.FieldSymbol( + name="person_id", + table=ast.LazyTableSymbol( + table=events_table_symbol, + joined_table=database.events.pdi, + field="pdi", + ), + ), + }, + tables={"events": events_table_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + + def test_resolve_lazy_events_pdi_table_aliased(self): + expr = parse_select("select event, e.pdi.person_id from events e") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol), + ), + ast.Field( + chain=["e", "pdi", "person_id"], + symbol=ast.FieldSymbol( + name="person_id", + table=ast.LazyTableSymbol( + table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="e", + symbol=events_table_alias_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_alias_symbol), + "person_id": ast.FieldSymbol( + name="person_id", + table=ast.LazyTableSymbol( + table=events_table_alias_symbol, + joined_table=database.events.pdi, + field="pdi", + ), + ), + }, + tables={"e": events_table_alias_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + + def test_resolve_lazy_events_pdi_person_table(self): + expr = parse_select("select event, pdi.person.id from events") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_symbol), + ), + ast.Field( + chain=["pdi", "person", "id"], + symbol=ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=ast.LazyTableSymbol( + table=events_table_symbol, field="pdi", joined_table=database.events.pdi + ), + field="person", + joined_table=database.events.pdi.table.person, + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + symbol=events_table_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_symbol), + "id": ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=ast.LazyTableSymbol( + table=events_table_symbol, field="pdi", joined_table=database.events.pdi + ), + field="person", + joined_table=database.events.pdi.table.person, + ), + ), + }, + tables={"events": events_table_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + + def test_resolve_lazy_events_pdi_person_table_aliased(self): + expr = parse_select("select event, e.pdi.person.id from events e") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol), + ), + ast.Field( + chain=["e", "pdi", "person", "id"], + symbol=ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=ast.LazyTableSymbol( + table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + ), + field="person", + joined_table=database.events.pdi.table.person, + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + alias="e", + symbol=events_table_alias_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_alias_symbol), + "id": ast.FieldSymbol( + name="id", + table=ast.LazyTableSymbol( + table=ast.LazyTableSymbol( + table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + ), + field="person", + joined_table=database.events.pdi.table.person, + ), + ), + }, + tables={"e": events_table_alias_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) + # "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" # "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" From 66a0e0504e550b49ef86c0114c9d28f338ddc315 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 19 Feb 2023 23:01:45 +0100 Subject: [PATCH 070/142] access joined subtables correctly --- posthog/hogql/ast.py | 2 + posthog/hogql/database.py | 13 +- posthog/hogql/printer.py | 44 +++---- posthog/hogql/test/test_query.py | 209 +++++++++++++++++++++++++------ 4 files changed, 204 insertions(+), 64 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 7921554c7b6c2..e9a8f7c56e209 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -169,6 +169,8 @@ class FieldSymbol(Symbol): def resolve_database_field(self) -> Optional[Union[DatabaseField, Table]]: table_symbol = self.table + if isinstance(table_symbol, LazyTableSymbol): + return table_symbol.joined_table.table.get_field(self.name) while isinstance(table_symbol, TableAliasSymbol): table_symbol = table_symbol.table if isinstance(table_symbol, TableSymbol): diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index b92fa4ececa40..2c1d8fb02bf4b 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -69,6 +69,7 @@ class Config: join_function: Callable[[str, str, List[str]], Any] table: Table + from_field: str class PersonsTable(Table): @@ -130,7 +131,9 @@ class PersonDistinctIdTable(Table): is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") - person: JoinedTable = JoinedTable(table=PersonsTable(), join_function=join_with_persons_table) + person: JoinedTable = JoinedTable( + from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table + ) def get_splash(self) -> Dict[str, DatabaseField]: splash: Dict[str, DatabaseField] = {} @@ -192,7 +195,9 @@ class EventsTable(Table): elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table) + pdi: JoinedTable = JoinedTable( + from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table + ) def clickhouse_table(self): return "events" @@ -216,7 +221,9 @@ class SessionRecordingEvents(Table): last_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="last_event_timestamp") urls: StringDatabaseField = StringDatabaseField(name="urls", array=True) - pdi: JoinedTable = JoinedTable(table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table) + pdi: JoinedTable = JoinedTable( + from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table + ) def clickhouse_table(self): return "session_recording_events" diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index fc2edd8239794..617f713ab6a40 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -364,14 +364,14 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): # :KLUDGE: Legacy person properties handling. Assume we're in a context where the tables have been joined, # and this "person_props" alias is accessible to us. - if resolved_field == database.events.person.properties: - if not self.context.using_person_on_events: - field_sql = "person_props" + # if resolved_field == database.events.pdi.table.person.table.properties: + # if not self.context.using_person_on_events: + # field_sql = "person_props" # If the field is called on a table that has an alias, prepend the table alias. # If there's another field with the same name in the scope that's not this, prepend the full table name. # Note: we don't prepend a table name for the special "person" fields. - elif isinstance(symbol.table, ast.TableAliasSymbol) or symbol_with_name_in_scope != symbol: + if isinstance(symbol.table, ast.TableAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" # TODO: refactor this legacy logging @@ -406,38 +406,34 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): def visit_property_symbol(self, symbol: ast.PropertySymbol): field_symbol = symbol.parent + field = field_symbol.resolve_database_field() key = f"hogql_val_{len(self.context.values)}" self.context.values[key] = symbol.name + # check for a materialised column table = field_symbol.table while isinstance(table, ast.TableAliasSymbol): table = table.table + if isinstance(table, ast.TableSymbol): + table_name = table.table.clickhouse_table() + if field is None: + raise ValueError(f"Can't resolve field {field_symbol.name} on table {table_name}") + field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) - if not isinstance(table, ast.TableSymbol): - raise ValueError(f"Unknown PropertySymbol table type: {type(table).__name__}") - - table_name = table.table.clickhouse_table() - - field = field_symbol.resolve_database_field() - if field is None: - raise ValueError(f"Can't resolve field {field_symbol.name} on table {table_name}") - - field_name = cast(Union[Literal["properties"], Literal["person_properties"]], field.name) - - if field_name == "person_properties" and not self.context.using_person_on_events: - # :KLUDGE: person property materialized columns support when person on events is off - materialized_column = self._get_materialized_column("person", symbol.name, "properties") - else: materialized_column = self._get_materialized_column(table_name, symbol.name, field_name) - - if materialized_column: - property_sql = self._print_identifier(materialized_column) + if materialized_column: + property_sql = self._print_identifier(materialized_column) + else: + field_sql = self.visit(field_symbol) + property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") - if field_name == "properties": + if not field: + pass + elif field.name == "properties": # TODO: refactor this legacy logging self.context.field_access_logs.append( HogQLFieldAccess( @@ -447,7 +443,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): property_sql, ) ) - elif field_name == "person_properties": + elif field.name == "person_properties": # TODO: refactor this legacy logging self.context.field_access_logs.append( HogQLFieldAccess( diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 4ca237bf61b7e..0dd62b9cd9f42 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -1,5 +1,8 @@ +from uuid import UUID + from freezegun import freeze_time +from posthog import datetime from posthog.hogql import ast from posthog.hogql.query import execute_hogql_query from posthog.models.utils import UUIDT @@ -160,68 +163,200 @@ def test_query_joins_pdi(self): ) self.assertTrue(len(response.results) > 0) - def test_query_joins_pdi_automatic(self): + def test_query_joins_events_pdi(self): with freeze_time("2020-01-10"): self._create_random_events() response = execute_hogql_query( - """ - SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10 - """, + "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10", self.team, ) + self.assertEqual( + response.hogql, + "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10", + ) self.assertEqual( response.clickhouse, - f"SELECT event, timestamp, events_pdi.distinct_id, events_pdi.person_id FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, 1) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS events_pdi ON equals(events.distinct_id, events_pdi.distinct_id) WHERE equals(team_id, {self.team.pk}) LIMIT 10", + f"SELECT event, timestamp, events__pdi.distinct_id, events__pdi.person_id FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) WHERE equals(team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][3], UUID("00000000-0000-4000-8000-000000000000")) + + def test_query_joins_events_e_pdi(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + "SELECT event, e.timestamp, e.pdi.distinct_id, pdi.person_id FROM events e LIMIT 10", + self.team, + ) + self.assertEqual( + response.hogql, "SELECT event, e.timestamp, e.pdi.distinct_id, pdi.person_id FROM events AS e LIMIT 10" + ) + self.assertEqual( + response.clickhouse, + f"SELECT e.event, e.timestamp, e__pdi.distinct_id, e__pdi.person_id FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) WHERE equals(e.team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][3], UUID("00000000-0000-4000-8000-000000000000")) + + def test_query_joins_pdi_persons(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + "SELECT pdi.distinct_id, pdi.person.created_at FROM person_distinct_ids pdi LIMIT 10", + self.team, ) self.assertEqual( response.hogql, - "SELECT event, timestamp, pdi.distinct_id, pdi.person_id FROM events LIMIT 10", + # TODO: store original db name in hogql + "SELECT pdi.distinct_id, pdi.person.created_at FROM person_distinct_id2 AS pdi LIMIT 10", + ) + self.assertEqual( + response.clickhouse, + f"SELECT pdi.distinct_id, pdi__person.created_at FROM person_distinct_id2 AS pdi INNER JOIN (SELECT argMax(person.created_at, version) AS created_at, id FROM person WHERE equals(team_id, {self.team.pk}) GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS pdi__person ON equals(pdi.person_id, pdi__person.id) WHERE equals(pdi.team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual(response.results[0][0], "bla") + self.assertEqual(response.results[0][1], datetime.datetime(2020, 1, 10, 0, 0)) + + def test_query_joins_pdi_person_properties(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + "SELECT pdi.distinct_id, pdi.person.properties.email FROM person_distinct_ids pdi LIMIT 10", + self.team, + ) + self.assertEqual( + response.hogql, + # TODO: store original db name in hogql + "SELECT pdi.distinct_id, pdi.person.properties.email FROM person_distinct_id2 AS pdi LIMIT 10", + ) + self.assertEqual( + response.clickhouse, + # TODO: properties should be extracted within the subquery + f"SELECT pdi.distinct_id, replaceRegexpAll(JSONExtractRaw(pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '') FROM person_distinct_id2 AS pdi INNER JOIN (SELECT argMax(person.properties, version) AS properties, id FROM person WHERE equals(team_id, {self.team.pk}) GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS pdi__person ON equals(pdi.person_id, pdi__person.id) WHERE equals(pdi.team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual(response.results[0][0], "bla") + self.assertEqual(response.results[0][1], "tim@posthog.com") + + def test_query_joins_events_pdi_person(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + "SELECT event, timestamp, pdi.distinct_id, pdi.person.id FROM events LIMIT 10", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT event, timestamp, events__pdi.distinct_id, events__pdi__person.id FROM events " + f"INNER JOIN (SELECT " + f"argMax(person_distinct_id2.person_id, version) AS person_id, " + f"distinct_id " + f"FROM person_distinct_id2 " + f"WHERE equals(team_id, {self.team.pk}) " + f"GROUP BY distinct_id " + f"HAVING equals(argMax(is_deleted, version), 0)" + f") AS events__pdi " + f"ON equals(events.distinct_id, events__pdi.distinct_id) " + f"INNER JOIN (" + f"SELECT id " + f"FROM person " + f"WHERE equals(team_id, {self.team.pk}) " + f"GROUP BY id " + f"HAVING equals(argMax(is_deleted, version), 0)" + f") AS events__pdi__person " + f"ON equals(events__pdi.person_id, events__pdi__person.id) " + f"WHERE equals(team_id, {self.team.pk}) " + f"LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT event, timestamp, pdi.distinct_id, pdi.person.id FROM events LIMIT 10", ) self.assertEqual(response.results[0][0], "random event") self.assertEqual(response.results[0][2], "bla") - self.assertEqual(response.results[0][4], "00000000-0000-4000-8000-000000000001") + self.assertEqual(response.results[0][3], UUID("00000000-0000-4000-8000-000000000000")) - def test_query_joins_person(self): + def test_query_joins_events_pdi_person_properties(self): with freeze_time("2020-01-10"): self._create_random_events() response = execute_hogql_query( - """ - SELECT event, timestamp, pdi.person_id from events e - INNER JOIN ( - SELECT distinct_id, - argMax(person_id, version) as person_id - FROM person_distinct_ids - GROUP BY distinct_id - HAVING argMax(is_deleted, version) = 0 - ) AS pdi - ON e.distinct_id = pdi.distinct_id - INNER JOIN ( - SELECT id - FROM person - WHERE team_id = 1 - AND id IN ( - SELECT id - FROM person - WHERE team_id = 1 - AND (has(['Chrome'], replaceRegexpAll(JSONExtractRaw(person.properties, '$browser'), '^"|"$', ''))) - ) - GROUP BY id - HAVING max(is_deleted) = 0 - AND (has(['Chrome'], replaceRegexpAll(JSONExtractRaw(argMax(person.properties, version), '$browser'), '^"|"$', ''))) - ) person - ON person.id = pdi.person_id - """, + "SELECT event, timestamp, pdi.distinct_id, pdi.person.properties.email FROM events LIMIT 10", self.team, ) + self.assertEqual( + response.clickhouse, + # TODO: properties should be extracted within the subquery + f"SELECT event, timestamp, events__pdi.distinct_id, replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, " + f"%(hogql_val_0)s), '^\"|\"$', '') FROM events INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) " + f"AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY distinct_id " + f"HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi ON equals(events.distinct_id, events__pdi.distinct_id) " + f"INNER JOIN (SELECT argMax(person.properties, version) AS properties, id FROM person WHERE equals(team_id, {self.team.pk}) " + f"GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi__person ON equals(events__pdi.person_id, " + f"events__pdi__person.id) WHERE equals(team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT event, timestamp, pdi.distinct_id, pdi.person.properties.email FROM events LIMIT 10", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][3], "tim@posthog.com") + + def test_query_joins_events_pdi_e_person_properties(self): + with freeze_time("2020-01-10"): + self._create_random_events() + response = execute_hogql_query( + "SELECT event, e.timestamp, pdi.distinct_id, e.pdi.person.properties.email FROM events e LIMIT 10", + self.team, + ) self.assertEqual( response.clickhouse, - f"SELECT e.event, e.timestamp, pdi.person_id FROM events AS e INNER JOIN (SELECT distinct_id, argMax(person_distinct_id2.person_id, version) AS person_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.id}) GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) WHERE equals(e.team_id, {self.team.id}) LIMIT 1000", + f"SELECT e.event, e.timestamp, e__pdi.distinct_id, replaceRegexpAll(JSONExtractRaw(e__pdi__person.properties, " + f"%(hogql_val_0)s), '^\"|\"$', '') FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, " + f"version) AS person_id, distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY " + f"distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) " + f"INNER JOIN (SELECT argMax(person.properties, version) AS properties, id FROM person WHERE equals(team_id, " + f"{self.team.pk}) GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS e__pdi__person ON " + f"equals(e__pdi.person_id, e__pdi__person.id) WHERE equals(e.team_id, {self.team.pk}) LIMIT 10", ) 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_id2 GROUP BY distinct_id HAVING equals(argMax(is_deleted, version), 0)) AS pdi ON equals(e.distinct_id, pdi.distinct_id) LIMIT 1000", + "SELECT event, e.timestamp, pdi.distinct_id, e.pdi.person.properties.email FROM events AS e LIMIT 10", ) - self.assertTrue(len(response.results) > 0) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "bla") + self.assertEqual(response.results[0][3], "tim@posthog.com") + + def test_query_joins_events_person_properties(self): + with freeze_time("2020-01-10"): + self._create_random_events() + + response = execute_hogql_query( + "SELECT event, e.timestamp, e.pdi.person.properties.email FROM events e LIMIT 10", + self.team, + ) + # import pdb; pdb.set_trace() + self.assertEqual( + response.clickhouse, + f"SELECT e.event, e.timestamp, replaceRegexpAll(JSONExtractRaw(e__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '') " + f"FROM events AS e INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id " + f"FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY distinct_id HAVING " + f"equals(argMax(is_deleted, version), 0)) AS e__pdi ON equals(e.distinct_id, e__pdi.distinct_id) " + f"INNER JOIN (SELECT argMax(person.properties, version) AS properties, id FROM person WHERE " + f"equals(team_id, {self.team.pk}) GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS e__pdi__person " + f"ON equals(e__pdi.person_id, e__pdi__person.id) WHERE equals(e.team_id, {self.team.pk}) LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT event, e.timestamp, e.pdi.person.properties.email FROM events AS e LIMIT 10", + ) + self.assertEqual(response.results[0][0], "random event") + self.assertEqual(response.results[0][2], "tim@posthog.com") From 87630408a19aaffc06e10a002ecf9d190b2e28fe Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 19 Feb 2023 23:03:18 +0100 Subject: [PATCH 071/142] resolve lazy tables --- posthog/hogql/transforms.py | 122 ++++++++++++++++++++++-------------- 1 file changed, 75 insertions(+), 47 deletions(-) diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index f22e1134d8db3..09103ba134b2d 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -41,65 +41,93 @@ def resolve_lazy_tables(node: ast.Expr): class LazyTableResolver(TraversingVisitor): def __init__(self): super().__init__() - self.fields: List[List[Union[ast.FieldSymbol]]] = [] + self.stack_of_fields: List[List[ast.FieldSymbol]] = [] + + def _get_long_table_name( + self, select: ast.SelectQuerySymbol, symbol: Union[ast.TableSymbol, ast.LazyTableSymbol, ast.TableAliasSymbol] + ) -> str: + if isinstance(symbol, ast.TableSymbol): + return select.key_for_table(symbol) + elif isinstance(symbol, ast.TableAliasSymbol): + return symbol.name + elif isinstance(symbol, ast.LazyTableSymbol): + return f"{self._get_long_table_name(select, symbol.table)}__{symbol.field}" + else: + raise ValueError("Should not be reachable") + + def visit_field_symbol(self, node: ast.FieldSymbol): + if isinstance(node.table, ast.LazyTableSymbol): + if len(self.stack_of_fields) == 0: + raise ValueError("Can't access a lazy field when not in a SelectQuery context") + self.stack_of_fields[-1].append(node) def visit_select_query(self, node: ast.SelectQuery): - if not node.symbol: + select_symbol = node.symbol + if not select_symbol: raise ValueError("Select query must have a symbol") # Collects each `ast.Field` with `ast.LazyTableSymbol` - lazy_fields: List[Union[ast.FieldSymbol]] = [] - self.fields.append(lazy_fields) + field_collector: List[ast.FieldSymbol] = [] + self.stack_of_fields.append(field_collector) super().visit_select_query(node) - last_join = node.select_from - while last_join.next_join is not None: - last_join = last_join.next_join - @dataclasses.dataclass - class LocalScope: + class JoinToAdd: fields_accessed: Set[str] join_function: Callable[[str, str, List[str]], ast.JoinExpr] - parent_key: str - table_alias: str - - new_tables: Dict[str, LocalScope] = {} - - for field in lazy_fields: - if not isinstance(field.table, ast.LazyTableSymbol): - raise ValueError("Should not be reachable.") - parent_key = node.symbol.key_for_table(field.table.table) - if parent_key is None: - raise ValueError("Should not be reachable.") - table_alias = f"{parent_key}_{field.table.field}" - if not new_tables.get(table_alias): - new_tables[table_alias] = LocalScope( - fields_accessed=set(), - join_function=field.table.joined_table.join_function, - parent_key=parent_key, - table_alias=table_alias, - ) - new_tables[table_alias].fields_accessed.add(field.name) - - for table_alias, scope in new_tables.items(): - next_join = scope.join_function(scope.parent_key, scope.table_alias, list(scope.fields_accessed)) - resolve_symbols(next_join, node.symbol) - node.symbol.tables[table_alias] = next_join.symbol # type: ignore - - last_join.next_join = next_join + from_table: str + from_field: str + to_table: str + + joins_to_add: Dict[str, JoinToAdd] = {} + + for field in field_collector: + lazy_table = field.table + # traverse the lazy tables to a real table, then loop over them in reverse order to create the joins + joins_for_field: List = [] + while isinstance(lazy_table, ast.LazyTableSymbol): + joins_for_field.append(lazy_table) + lazy_table = lazy_table.table + for lazy_table in reversed(joins_for_field): + from_table = self._get_long_table_name(select_symbol, lazy_table.table) + to_table = self._get_long_table_name(select_symbol, lazy_table) + if to_table not in joins_to_add: + joins_to_add[to_table] = JoinToAdd( + fields_accessed=set(), + join_function=lazy_table.joined_table.join_function, + from_table=from_table, + from_field=lazy_table.joined_table.from_field, + to_table=to_table, + ) + new_join = joins_to_add[to_table] + if lazy_table == field.table: + new_join.fields_accessed.add(field.name) + + # Make sure we also add the join "ON" condition fields into the list of fields accessed. + # Without this "events.pdi.person.anything" won't work without ALSO selecting "events.pdi.person_id" explicitly + 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.add(new_join.from_field) + + last_join = node.select_from + while last_join and last_join.next_join is not None: + last_join = last_join.next_join + + for to_table, scope in joins_to_add.items(): + next_join = scope.join_function(scope.from_table, scope.to_table, list(scope.fields_accessed)) + resolve_symbols(next_join, select_symbol) + select_symbol.tables[to_table] = next_join.symbol + 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 field in lazy_fields: - parent_key = node.symbol.key_for_table(field.table.table) - table_alias = f"{parent_key}_{field.table.field}" - field.table = node.symbol.tables[table_alias] + for field in field_collector: + to_table = self._get_long_table_name(select_symbol, field.table) + field.table = select_symbol.tables[to_table] - self.fields.pop() - - def visit_field_symbol(self, node: ast.FieldSymbol): - if isinstance(node.table, ast.LazyTableSymbol): - if len(self.fields) == 0: - raise ValueError("Can't access a lazy field when not in a SelectQuery context") - self.fields[-1].append(node) + self.stack_of_fields.pop() From b97bcc6cbbc84cb98263fa8066ddf409ba72085b Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 19 Feb 2023 23:17:44 +0100 Subject: [PATCH 072/142] fix bug --- posthog/hogql/ast.py | 4 +++- posthog/hogql/test/test_query.py | 25 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index e9a8f7c56e209..c357cd3650b6f 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -182,7 +182,9 @@ def get_child(self, name: str) -> Symbol: if database_field is None: raise ValueError(f'Can not access property "{name}" on field "{self.name}".') if isinstance(database_field, JoinedTable): - return FieldSymbol(name=name, table=LazyTableSymbol(table=self, field=name, joined_table=database_field)) + return FieldSymbol( + name=name, table=LazyTableSymbol(table=self.table, field=name, joined_table=database_field) + ) if isinstance(database_field, StringJSONDatabaseField): return PropertySymbol(name=name, parent=self) raise ValueError( diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index 0dd62b9cd9f42..b21a0e224322c 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -343,7 +343,6 @@ def test_query_joins_events_person_properties(self): "SELECT event, e.timestamp, e.pdi.person.properties.email FROM events e LIMIT 10", self.team, ) - # import pdb; pdb.set_trace() self.assertEqual( response.clickhouse, f"SELECT e.event, e.timestamp, replaceRegexpAll(JSONExtractRaw(e__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '') " @@ -360,3 +359,27 @@ def test_query_joins_events_person_properties(self): ) self.assertEqual(response.results[0][0], "random event") self.assertEqual(response.results[0][2], "tim@posthog.com") + + def test_query_joins_events_person_properties_in_aggregration(self): + with freeze_time("2020-01-10"): + self._create_random_events() + response = execute_hogql_query( + "SELECT s.pdi.person.properties.email, count() FROM events s GROUP BY s.pdi.person.properties.email LIMIT 10", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT replaceRegexpAll(JSONExtractRaw(s__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', ''), " + f"count(*) FROM events AS s INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, " + f"distinct_id FROM person_distinct_id2 WHERE equals(team_id, {self.team.pk}) GROUP BY distinct_id HAVING " + f"equals(argMax(is_deleted, version), 0)) AS s__pdi ON equals(s.distinct_id, s__pdi.distinct_id) INNER JOIN " + f"(SELECT argMax(person.properties, version) AS properties, id FROM person WHERE equals(team_id, {self.team.pk}) " + f"GROUP BY id HAVING equals(argMax(is_deleted, version), 0)) AS s__pdi__person ON " + f"equals(s__pdi.person_id, s__pdi__person.id) WHERE equals(s.team_id, {self.team.pk}) GROUP BY " + f"replaceRegexpAll(JSONExtractRaw(s__pdi__person.properties, %(hogql_val_1)s), '^\"|\"$', '') LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT s.pdi.person.properties.email, count() FROM events AS s GROUP BY s.pdi.person.properties.email LIMIT 10", + ) + self.assertEqual(response.results[0][0], "tim@posthog.com") From 1978c27a11af93e5d0287a9dbc57d8b4e56bc079 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Sun, 19 Feb 2023 23:49:01 +0100 Subject: [PATCH 073/142] add groups table --- .../src/queries/nodes/DataTable/renderColumnMeta.tsx | 2 +- posthog/hogql/database.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx index d98200408e47a..c16df2ff7ff88 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx @@ -17,7 +17,7 @@ export function renderColumnMeta(key: string, query: DataTableNode, context?: Qu if (key === 'timestamp') { title = 'Time' } else if (key === 'created_at') { - title = 'First seen' + title = 'Created at' } else if (key === 'event') { title = 'Event' } else if (key === 'person') { diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 2c1d8fb02bf4b..98b8852c889e8 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -185,6 +185,17 @@ def join_with_max_person_distinct_id_table(from_table: str, to_table: str, reque ) +class GroupsTable(Table): + group_type_index: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + group_key: StringDatabaseField = StringDatabaseField(name="group_key") + created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") + team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") + group_properties: StringJSONDatabaseField = StringJSONDatabaseField(name="group_properties") + + def clickhouse_table(self): + return "groups" + + class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") event: StringDatabaseField = StringDatabaseField(name="event") @@ -238,6 +249,7 @@ class Config: persons: PersonsTable = PersonsTable() person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() + groups: GroupsTable = GroupsTable() def has_table(self, table_name: str) -> bool: return hasattr(self, table_name) From f81e9fe03566d55c902ed21e4e86e98e3358e82d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 00:01:07 +0100 Subject: [PATCH 074/142] support intervals --- posthog/hogql/parser.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index b7be7d161938a..de1eef6975045 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -408,7 +408,26 @@ def visitColumnExprPrecedence3(self, ctx: HogQLParser.ColumnExprPrecedence3Conte return ast.CompareOperation(left=self.visit(ctx.left), right=self.visit(ctx.right), op=op) def visitColumnExprInterval(self, ctx: HogQLParser.ColumnExprIntervalContext): - raise NotImplementedError(f"Unsupported node: ColumnExprInterval") + if ctx.interval().SECOND(): + name = "toIntervalSecond" + elif ctx.interval().MINUTE(): + name = "toIntervalMinute" + elif ctx.interval().HOUR(): + name = "toIntervalHour" + elif ctx.interval().DAY(): + name = "toIntervalDay" + elif ctx.interval().WEEK(): + name = "toIntervalWeek" + elif ctx.interval().MONTH(): + name = "toIntervalMonth" + elif ctx.interval().QUARTER(): + name = "toIntervalQuarter" + elif ctx.interval().YEAR(): + name = "toIntervalYear" + else: + raise NotImplementedError(f"Unsupported interval type: {ctx.interval().getText()}") + + return ast.Call(name=name, args=[self.visit(ctx.columnExpr())]) def visitColumnExprIsNull(self, ctx: HogQLParser.ColumnExprIsNullContext): return ast.CompareOperation( From 4c0e109c1ab9e974c82481f27feec7a6becc3c80 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 00:17:16 +0100 Subject: [PATCH 075/142] prettier for sql --- frontend/src/queries/examples.ts | 15 +++++++++-- package.json | 1 + pnpm-lock.yaml | 45 +++++++++++++++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 91077af07054d..c56f5794f7578 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -293,13 +293,24 @@ const TimeToSeeDataWaterfall: TimeToSeeDataWaterfallNode = { const HogQL: HogQLQuery = { kind: NodeKind.HogQLQuery, - query: 'select event, count() as event_count from events group by event order by event_count desc', + query: 'select event, count() from events where timestamp > now() - interval 1 month group by event order by count() desc', } const HogQLTable: DataTableNode = { kind: NodeKind.DataTableNode, full: true, - source: HogQL, + source: { + kind: NodeKind.HogQLQuery, + query: ` select event, + pdi.person.properties.email, + count() + from events + where timestamp > now () - interval 1 month + and pdi.person.properties.email is not null + group by event, + pdi.person.properties.email + order by count() desc`, + }, } export const examples: Record = { diff --git a/package.json b/package.json index 780fc5f794c17..7af28ccd262ab 100644 --- a/package.json +++ b/package.json @@ -141,6 +141,7 @@ "resize-observer-polyfill": "^1.5.1", "rrweb": "^1.1.3", "sass": "^1.26.2", + "sql-formatter": "^12.1.2", "use-debounce": "^6.0.1", "use-resize-observer": "^8.0.0", "wildcard-match": "^5.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f3cd2dd9b839..18bcf47abd99d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,7 @@ specifiers: rrweb: ^1.1.3 sass: ^1.26.2 sass-loader: ^10.0.1 + sql-formatter: ^12.1.2 storybook-addon-pseudo-states: ^1.15.1 style-loader: ^2.0.0 sucrase: ^3.29.0 @@ -269,6 +270,7 @@ dependencies: resize-observer-polyfill: 1.5.1 rrweb: 1.1.3 sass: 1.56.0 + sql-formatter: 12.1.2 use-debounce: 6.0.1_react@16.14.0 use-resize-observer: 8.0.0_wcqkhtmu7mswc6yz4uyexck3ty wildcard-match: 5.1.2 @@ -5643,6 +5645,10 @@ packages: sprintf-js: 1.0.3 dev: true + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + /aria-hidden/1.2.1_edij4neeagymnxmr7qklvezyj4: resolution: {integrity: sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==} engines: {node: '>=10'} @@ -8190,6 +8196,10 @@ packages: path-type: 4.0.0 dev: true + /discontinuous-range/1.0.0: + resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} + dev: false + /doctrine/2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -13283,6 +13293,10 @@ packages: color-name: 1.1.4 dev: true + /moo/0.5.2: + resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + dev: false + /move-concurrently/1.0.1: resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} dependencies: @@ -13415,6 +13429,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /nearley/2.20.1: + resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} + hasBin: true + dependencies: + commander: 2.20.3 + moo: 0.5.2 + railroad-diagrams: 1.0.0 + randexp: 0.4.6 + dev: false + /needle/3.2.0: resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==} engines: {node: '>= 4.4.x'} @@ -14899,10 +14923,22 @@ packages: engines: {node: '>=10'} dev: false + /railroad-diagrams/1.0.0: + resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + dev: false + /ramda/0.28.0: resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} dev: true + /randexp/0.4.6: + resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} + engines: {node: '>=0.12'} + dependencies: + discontinuous-range: 1.0.0 + ret: 0.1.15 + dev: false + /randombytes/2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -16240,7 +16276,6 @@ packages: /ret/0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} - dev: true /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -16844,6 +16879,14 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /sql-formatter/12.1.2: + resolution: {integrity: sha512-SoFn+9ZflUt8+HYZ/PaifXt1RptcDUn8HXqsWmfXdPV3WeHPgT0qOSJXxHU24d7NOVt9X40MLqf263fNk79XqA==} + hasBin: true + dependencies: + argparse: 2.0.1 + nearley: 2.20.1 + dev: false + /sshpk/1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} engines: {node: '>=0.10.0'} From 3fd47f4b2a90422389237cca736d7142dcd32b6d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 00:17:37 +0100 Subject: [PATCH 076/142] prettier for sql --- .../nodes/HogQLQuery/HogQLQueryEditor.tsx | 2 +- .../nodes/HogQLQuery/hogQLQueryEditorLogic.ts | 20 +++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index 2ee10bc896419..51faa0b4d47ef 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -19,7 +19,7 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { const { setQueryInput, saveQuery } = useActions(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps)) return ( -
+
{({ height }) => ( diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index 474926ec9f1d3..5d8f94ff43edb 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -2,6 +2,17 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers } fro import { HogQLQuery } from '~/queries/schema' import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' +import { format } from 'sql-formatter' + +function formatSQL(sql: string): string { + return format(sql, { + language: 'sql', + tabWidth: 2, + keywordCase: 'preserve', + linesBetweenQueries: 2, + indentStyle: 'tabularRight', + }) +} export interface HogQLQueryEditorLogicProps { key: number @@ -15,7 +26,7 @@ export const hogQLQueryEditorLogic = kea([ key((props) => props.key), propsChanged(({ actions, props }, oldProps) => { if (props.query.query !== oldProps.query.query) { - actions.setQueryInput(props.query.query) + actions.setQueryInput(formatSQL(props.query.query)) } }), actions({ @@ -23,12 +34,13 @@ export const hogQLQueryEditorLogic = kea([ setQueryInput: (queryInput: string) => ({ queryInput }), }), reducers(({ props }) => ({ - queryInput: [props.query.query, { setQueryInput: (_, { queryInput }) => queryInput }], + queryInput: [formatSQL(props.query.query), { setQueryInput: (_, { queryInput }) => queryInput }], })), listeners(({ actions, props, values }) => ({ saveQuery: () => { - actions.setQueryInput(values.queryInput) - props.setQuery?.({ ...props.query, query: values.queryInput }) + const formattedQuery = formatSQL(values.queryInput) + actions.setQueryInput(formattedQuery) + props.setQuery?.({ ...props.query, query: formattedQuery }) }, })), ]) From d6b8d3ecb510e621d5b13d30639bf49e4e714f86 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 00:22:34 +0100 Subject: [PATCH 077/142] add failing test --- posthog/hogql/test/test_transforms.py | 58 +++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 posthog/hogql/test/test_transforms.py diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py new file mode 100644 index 0000000000000..f0ee16ab24ff8 --- /dev/null +++ b/posthog/hogql/test/test_transforms.py @@ -0,0 +1,58 @@ +from posthog.hogql import ast +from posthog.hogql.database import database +from posthog.hogql.parser import parse_select +from posthog.hogql.resolver import resolve_symbols +from posthog.hogql.transforms import resolve_lazy_tables +from posthog.test.base import BaseTest + + +class TestTransforms(BaseTest): + def test_resolve_lazy_tables(self): + expr = parse_select("select event, pdi.person_id from events") + resolve_symbols(expr) + resolve_lazy_tables(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + next_join = database.events.pdi.join_function("events", "events__pdi", ["person_id"]) + # resolve_symbols(next_join, expr.symbol) + + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_symbol), + ), + ast.Field( + chain=["person_id"], + symbol=ast.FieldSymbol( + name="person_id", + table=next_join.table.symbol, + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + symbol=events_table_symbol, + next_join=next_join, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_symbol), + "person_id": ast.FieldSymbol( + name="person_id", + table=ast.LazyTableSymbol( + table=events_table_symbol, + joined_table=database.events.pdi, + field="pdi", + ), + ), + }, + tables={"events": events_table_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) From f72bf635a44cc180772477d242029b83367b319e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 00:32:13 +0100 Subject: [PATCH 078/142] support dollars --- frontend/src/queries/examples.ts | 2 ++ frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx | 2 +- frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index c56f5794f7578..d58c6fce4b2d9 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -303,11 +303,13 @@ const HogQLTable: DataTableNode = { kind: NodeKind.HogQLQuery, query: ` select event, pdi.person.properties.email, + properties.$browser, count() from events where timestamp > now () - interval 1 month and pdi.person.properties.email is not null group by event, + properties.$browser, pdi.person.properties.email order by count() desc`, }, diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index 51faa0b4d47ef..0d38c3b4beeab 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -26,7 +26,7 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { setQueryInput(v ?? '')} height={height} diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index 5d8f94ff43edb..9a457ed4572b6 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -6,7 +6,7 @@ import { format } from 'sql-formatter' function formatSQL(sql: string): string { return format(sql, { - language: 'sql', + language: 'mysql', tabWidth: 2, keywordCase: 'preserve', linesBetweenQueries: 2, From 947e7bf8d235003570d66f238410d98df52f9088 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:07:39 +0100 Subject: [PATCH 079/142] asterisk and obelisk --- posthog/hogql/ast.py | 2 +- posthog/hogql/database.py | 6 +++--- posthog/hogql/printer.py | 12 ++++++------ posthog/hogql/resolver.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 2d55753b9d851..d47bd578e0261 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -118,7 +118,7 @@ class ConstantSymbol(Symbol): value: Any -class SplashSymbol(Symbol): +class AsteriskSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 7341258bb58bc..76b735e7578a9 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -48,16 +48,16 @@ def get_field(self, name: str) -> DatabaseField: def clickhouse_table(self): raise NotImplementedError("Table.clickhouse_table not overridden") - def get_splash(self) -> List[str]: + def get_asterisk(self) -> List[str]: list: List[str] = [] for field in self.__fields__.values(): database_field = field.default if isinstance(database_field, DatabaseField): list.append(database_field.name) elif isinstance(database_field, Table): - list.extend(database_field.get_splash()) + list.extend(database_field.get_asterisk()) else: - raise ValueError(f"Unknown field type {type(database_field).__name__} for splash") + raise ValueError(f"Unknown field type {type(database_field).__name__} for asterisk") return list diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index bce2d1a9b25b3..f4d9670cc80e9 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -351,10 +351,10 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if isinstance(resolved_field, Table): # :KLUDGE: only works for events.person.* printing now if isinstance(symbol.table, ast.TableSymbol): - return self.visit(ast.SplashSymbol(table=ast.TableSymbol(table=resolved_field))) + return self.visit(ast.AsteriskSymbol(table=ast.TableSymbol(table=resolved_field))) else: return self.visit( - ast.SplashSymbol( + ast.AsteriskSymbol( table=ast.TableAliasSymbol( table=ast.TableSymbol(table=resolved_field), name=symbol.table.name ) @@ -467,17 +467,17 @@ def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return self._print_identifier(symbol.name) - def visit_splash_symbol(self, symbol: ast.SplashSymbol): + def visit_asterisk_symbol(self, symbol: ast.AsteriskSymbol): table = symbol.table while isinstance(table, ast.TableAliasSymbol): table = table.table if not isinstance(table, ast.TableSymbol): - raise ValueError(f"Unknown SplashSymbol table type: {type(table).__name__}") - splash_fields = table.table.get_splash() + raise ValueError(f"Unknown AsteriskSymbol table type: {type(table).__name__}") + asterisk_fields = table.table.get_asterisk() prefix = ( f"{self._print_identifier(symbol.table.name)}." if isinstance(symbol.table, ast.TableAliasSymbol) else "" ) - return f"tuple({', '.join(f'{prefix}{self._print_identifier(field)}' for field in splash_fields)})" + return f"tuple({', '.join(f'{prefix}{self._print_identifier(field)}' for field in asterisk_fields)})" def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index e08e6215a64ed..341235000607e 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -167,7 +167,7 @@ def visit_field(self, node): if table_count > 1: raise ResolverException("Cannot use '*' when there are multiple tables in the query") table = scope.anonymous_tables[0] if len(scope.anonymous_tables) > 0 else list(scope.tables.values())[0] - symbol = ast.SplashSymbol(table=table) + symbol = ast.AsteriskSymbol(table=table) if not symbol: symbol = lookup_field_by_name(scope, name) From 74bf09cbfa631f990020d975cf7c81dfeb5311be Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:08:26 +0100 Subject: [PATCH 080/142] yeet --- posthog/hogql/test/test_resolver.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 8e4fd16cf095a..9bc9e43e34181 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -234,17 +234,3 @@ def test_resolve_errors(self): with self.assertRaises(ResolverException) as e: resolve_symbols(parse_select(query)) self.assertIn("Unable to resolve field:", str(e.exception)) - - -# "with 2 as a select 1 as a" -> "Different expressions with the same alias a:" -# "with 2 as b, 3 as c select (select 1 as b) as a, b, c" -> "Different expressions with the same alias b:" -# "select a, b, e.c from (select 1 as a, 2 as b, 3 as c) as e" -> 1, 2, 3 - -# # good -# SELECT t.x FROM (SELECT 1 AS x) AS t; -# SELECT t.x FROM (SELECT x FROM tbl) AS t; -# SELECT x FROM (SELECT x FROM tbl) AS t; -# SELECT 1 AS x, x, x + 1; -# SELECT x, x + 1, 1 AS x; -# SELECT x, 1 + (2 + (3 AS x)); -# "SELECT x IN (SELECT 1 AS x) FROM (SELECT 1 AS x)", From d8eb09129b0371545a281edf382a0e2301fca4da Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:09:57 +0100 Subject: [PATCH 081/142] class is for internal use only --- posthog/hogql/printer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index f4d9670cc80e9..189038c992540 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -46,7 +46,7 @@ def print_ast( # TODO: add joins to person and group tables pass - return Printer(context=context, dialect=dialect, stack=stack or []).visit(node) + return _Printer(context=context, dialect=dialect, stack=stack or []).visit(node) @dataclass @@ -55,7 +55,7 @@ class JoinExprResponse: where: Optional[ast.Expr] = None -class Printer(Visitor): +class _Printer(Visitor): # NOTE: Call "print_ast()", not this class directly. def __init__( From 9f1ae92fa92e75b1383e6f59cd0febbdcaa35fa5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:15:54 +0100 Subject: [PATCH 082/142] fix aliases --- posthog/hogql/printer.py | 5 ++++- posthog/hogql/test/test_printer.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 189038c992540..ec7f03fe09aa4 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -329,7 +329,10 @@ def visit_placeholder(self, node: ast.Placeholder): raise ValueError(f"Found a Placeholder {{{node.field}}} in the tree. Can't generate query!") def visit_alias(self, node: ast.Alias): - return f"{self.visit(node.expr)} AS {self._print_identifier(node.alias)}" + inside = self.visit(node.expr) + if isinstance(node.expr, ast.Alias): + inside = f"({inside})" + return f"{inside} AS {self._print_identifier(node.alias)}" def visit_table_symbol(self, symbol: ast.TableSymbol): return self._print_identifier(symbol.table.clickhouse_table()) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 6a8ec04c86290..46576e453e640 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -224,8 +224,6 @@ def test_expr_syntax_errors(self): "select query from events", "line 1, column 13: mismatched input 'from' expecting " ) self._assert_expr_error("this makes little sense", "Unable to resolve field: this") - # TODO: fix - # self._assert_expr_error("event makes little sense", "event AS makes AS little AS sense") self._assert_expr_error("1;2", "line 1, column 1: mismatched input ';' expecting") self._assert_expr_error("b.a(bla)", "SyntaxError: line 1, column 3: mismatched input '(' expecting '.'") @@ -360,6 +358,8 @@ def test_alias_keywords(self): self._select("select 1 as `-- select team_id` from events"), "SELECT 1 AS `-- select team_id` FROM events WHERE equals(team_id, 42) LIMIT 65535", ) + # Some aliases are funny, but that's what the antlr syntax permits, and ClickHouse doesn't complain either + self.assertEqual(self._expr("event makes little sense"), "((event AS makes) AS little) AS sense") def test_select(self): self.assertEqual(self._select("select 1"), "SELECT 1 LIMIT 65535") From c767159eeaedff1bf7e9ae82efe1d926b48c5edd Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:16:11 +0100 Subject: [PATCH 083/142] not needed --- posthog/hogql/ast.py | 1 - 1 file changed, 1 deletion(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d47bd578e0261..6c265dfeb4b45 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -105,7 +105,6 @@ def has_child(self, name: str) -> bool: return self.symbol.has_child(name) -SelectQuerySymbol.update_forward_refs(SelectQuerySymbol=SelectQuerySymbol) SelectQuerySymbol.update_forward_refs(SelectQueryAliasSymbol=SelectQueryAliasSymbol) From e496907e05579770354037fb172bc03fb3fa294a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:21:29 +0100 Subject: [PATCH 084/142] fix a few fields --- posthog/hogql/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 76b735e7578a9..186241ef621cf 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -117,10 +117,10 @@ class SessionRecordingEvents(Table): distinct_id: StringDatabaseField = StringDatabaseField(name="distinct_id") session_id: StringDatabaseField = StringDatabaseField(name="session_id") window_id: StringDatabaseField = StringDatabaseField(name="window_id") - snapshot_data: StringDatabaseField = StringDatabaseField(name="snapshot_data") + snapshot_data: StringJSONDatabaseField = StringJSONDatabaseField(name="snapshot_data") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") has_full_snapshot: BooleanDatabaseField = BooleanDatabaseField(name="has_full_snapshot") - events_summary: BooleanDatabaseField = BooleanDatabaseField(name="events_summary", array=True) + events_summary: StringJSONDatabaseField = StringJSONDatabaseField(name="events_summary", array=True) click_count: IntegerDatabaseField = IntegerDatabaseField(name="click_count") keypress_count: IntegerDatabaseField = IntegerDatabaseField(name="keypress_count") timestamps_summary: DateTimeDatabaseField = DateTimeDatabaseField(name="timestamps_summary", array=True) From 4e3830c3a201337aec041ce3528fa8820559b850 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:56:11 +0100 Subject: [PATCH 085/142] pretty printing --- .../nodes/HogQLQuery/HogQLQueryEditor.tsx | 4 +- .../nodes/HogQLQuery/hogQLQueryEditorLogic.ts | 19 ++++++-- package.json | 1 + pnpm-lock.yaml | 45 ++++++++++++++++++- 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx index 2ee10bc896419..0d38c3b4beeab 100644 --- a/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx +++ b/frontend/src/queries/nodes/HogQLQuery/HogQLQueryEditor.tsx @@ -19,14 +19,14 @@ export function HogQLQueryEditor(props: HogQLQueryEditorProps): JSX.Element { const { setQueryInput, saveQuery } = useActions(hogQLQueryEditorLogic(hogQLQueryEditorLogicProps)) return ( -
+
{({ height }) => ( setQueryInput(v ?? '')} height={height} diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index 474926ec9f1d3..566c2dfaa51fb 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -1,8 +1,18 @@ import { actions, kea, key, listeners, path, props, propsChanged, reducers } from 'kea' +import { format } from 'sql-formatter' import { HogQLQuery } from '~/queries/schema' import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' +function formatSQL(sql: string): string { + return format(sql, { + language: 'mysql', + tabWidth: 2, + keywordCase: 'preserve', + linesBetweenQueries: 2, + indentStyle: 'tabularRight', + }) +} export interface HogQLQueryEditorLogicProps { key: number query: HogQLQuery @@ -15,7 +25,7 @@ export const hogQLQueryEditorLogic = kea([ key((props) => props.key), propsChanged(({ actions, props }, oldProps) => { if (props.query.query !== oldProps.query.query) { - actions.setQueryInput(props.query.query) + actions.setQueryInput(formatSQL(props.query.query)) } }), actions({ @@ -23,12 +33,13 @@ export const hogQLQueryEditorLogic = kea([ setQueryInput: (queryInput: string) => ({ queryInput }), }), reducers(({ props }) => ({ - queryInput: [props.query.query, { setQueryInput: (_, { queryInput }) => queryInput }], + queryInput: [formatSQL(props.query.query), { setQueryInput: (_, { queryInput }) => queryInput }], })), listeners(({ actions, props, values }) => ({ saveQuery: () => { - actions.setQueryInput(values.queryInput) - props.setQuery?.({ ...props.query, query: values.queryInput }) + const formattedQuery = formatSQL(values.queryInput) + actions.setQueryInput(formattedQuery) + props.setQuery?.({ ...props.query, query: formattedQuery }) }, })), ]) diff --git a/package.json b/package.json index 9fde08be2475f..e680edc16a0ab 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,7 @@ "resize-observer-polyfill": "^1.5.1", "rrweb": "^1.1.3", "sass": "^1.26.2", + "sql-formatter": "^12.1.2", "use-debounce": "^6.0.1", "use-resize-observer": "^8.0.0", "wildcard-match": "^5.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 331ac49dac7d4..75d70327432c1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,7 @@ specifiers: rrweb: ^1.1.3 sass: ^1.26.2 sass-loader: ^10.0.1 + sql-formatter: ^12.1.2 storybook-addon-pseudo-states: ^1.15.1 style-loader: ^2.0.0 sucrase: ^3.29.0 @@ -269,6 +270,7 @@ dependencies: resize-observer-polyfill: 1.5.1 rrweb: 1.1.3 sass: 1.56.0 + sql-formatter: 12.1.2 use-debounce: 6.0.1_react@16.14.0 use-resize-observer: 8.0.0_wcqkhtmu7mswc6yz4uyexck3ty wildcard-match: 5.1.2 @@ -5643,6 +5645,10 @@ packages: sprintf-js: 1.0.3 dev: true + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false + /aria-hidden/1.2.1_edij4neeagymnxmr7qklvezyj4: resolution: {integrity: sha512-PN344VAf9j1EAi+jyVHOJ8XidQdPVssGco39eNcsGdM4wcsILtxrKLkbuiMfLWYROK1FjRQasMWCBttrhjnr6A==} engines: {node: '>=10'} @@ -8190,6 +8196,10 @@ packages: path-type: 4.0.0 dev: true + /discontinuous-range/1.0.0: + resolution: {integrity: sha512-c68LpLbO+7kP/b1Hr1qs8/BJ09F5khZGTxqxZuhzxpmwJKOgRFHJWIb9/KmqnqHhLdO55aOxFH/EGBvUQbL/RQ==} + dev: false + /doctrine/2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} @@ -13283,6 +13293,10 @@ packages: color-name: 1.1.4 dev: true + /moo/0.5.2: + resolution: {integrity: sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==} + dev: false + /move-concurrently/1.0.1: resolution: {integrity: sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==} dependencies: @@ -13415,6 +13429,16 @@ packages: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true + /nearley/2.20.1: + resolution: {integrity: sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==} + hasBin: true + dependencies: + commander: 2.20.3 + moo: 0.5.2 + railroad-diagrams: 1.0.0 + randexp: 0.4.6 + dev: false + /needle/3.2.0: resolution: {integrity: sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==} engines: {node: '>= 4.4.x'} @@ -14899,10 +14923,22 @@ packages: engines: {node: '>=10'} dev: false + /railroad-diagrams/1.0.0: + resolution: {integrity: sha512-cz93DjNeLY0idrCNOH6PviZGRN9GJhsdm9hpn1YCS879fj4W+x5IFJhhkRZcwVgMmFF7R82UA/7Oh+R8lLZg6A==} + dev: false + /ramda/0.28.0: resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==} dev: true + /randexp/0.4.6: + resolution: {integrity: sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==} + engines: {node: '>=0.12'} + dependencies: + discontinuous-range: 1.0.0 + ret: 0.1.15 + dev: false + /randombytes/2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: @@ -16240,7 +16276,6 @@ packages: /ret/0.1.15: resolution: {integrity: sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==} engines: {node: '>=0.12'} - dev: true /reusify/1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} @@ -16844,6 +16879,14 @@ packages: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} dev: true + /sql-formatter/12.1.2: + resolution: {integrity: sha512-SoFn+9ZflUt8+HYZ/PaifXt1RptcDUn8HXqsWmfXdPV3WeHPgT0qOSJXxHU24d7NOVt9X40MLqf263fNk79XqA==} + hasBin: true + dependencies: + argparse: 2.0.1 + nearley: 2.20.1 + dev: false + /sshpk/1.17.0: resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} engines: {node: '>=0.10.0'} From b25fd20ff42ebf32e2f99a11c040d2837c6f05a6 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 20 Feb 2023 23:59:19 +0100 Subject: [PATCH 086/142] update sample query --- frontend/src/queries/examples.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 91077af07054d..5e1847149c1df 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -293,7 +293,14 @@ const TimeToSeeDataWaterfall: TimeToSeeDataWaterfallNode = { const HogQL: HogQLQuery = { kind: NodeKind.HogQLQuery, - query: 'select event, count() as event_count from events group by event order by event_count desc', + query: + ' select event,\n' + + ' properties.$geoip_country_name as `Country Name`,\n' + + ' count() as `Event count`\n' + + ' from events\n' + + ' group by event,\n' + + ' properties.$geoip_country_name\n' + + ' order by count() desc', } const HogQLTable: DataTableNode = { From bf29398dd5e5d58d1878712df65b7e58d8af80a5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 23:04:31 +0000 Subject: [PATCH 087/142] Update snapshots --- posthog/api/test/__snapshots__/test_feature_flag.ambr | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_feature_flag.ambr b/posthog/api/test/__snapshots__/test_feature_flag.ambr index df1466bd75a1c..4669865ffa923 100644 --- a/posthog/api/test/__snapshots__/test_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_feature_flag.ambr @@ -405,7 +405,9 @@ ' SELECT pg_sleep(1); - SELECT ("posthog_person"."properties" -> 'email') = '"tim@posthog.com"' AS "flag_X_condition_0", + SELECT (("posthog_person"."properties" -> 'email') = '"tim@posthog.com"' + AND "posthog_person"."properties" ? 'email' + AND NOT (("posthog_person"."properties" -> 'email') = 'null')) AS "flag_X_condition_0", (true) AS "flag_X_condition_0" FROM "posthog_person" INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") @@ -443,7 +445,9 @@ --- # name: TestResiliency.test_feature_flags_v3_with_experience_continuity_working_slow_db.3 ' - SELECT ("posthog_person"."properties" -> 'email') = '"tim@posthog.com"' AS "flag_X_condition_0", + SELECT (("posthog_person"."properties" -> 'email') = '"tim@posthog.com"' + AND "posthog_person"."properties" ? 'email' + AND NOT (("posthog_person"."properties" -> 'email') = 'null')) AS "flag_X_condition_0", (true) AS "flag_X_condition_0" FROM "posthog_person" INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") From f21c9288ba66bba7dcdbda6eb3004e5a5eff5f0f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 00:10:03 +0100 Subject: [PATCH 088/142] update sample query --- frontend/src/queries/examples.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 5e1847149c1df..c53e9ea62a590 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -300,7 +300,8 @@ const HogQL: HogQLQuery = { ' from events\n' + ' group by event,\n' + ' properties.$geoip_country_name\n' + - ' order by count() desc', + ' order by count() desc\n' + + ' limit 100', } const HogQLTable: DataTableNode = { From b35777f81b7810a7085f90d673ccc76645784bed Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 20 Feb 2023 23:11:45 +0000 Subject: [PATCH 089/142] Update snapshots --- posthog/api/test/__snapshots__/test_decide.ambr | 8 ++++++-- posthog/api/test/__snapshots__/test_insight.ambr | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 8d2a98767cd22..a1d88aebb96d2 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -589,7 +589,9 @@ --- # name: TestDecide.test_flag_with_regular_cohorts.5 ' - SELECT ("posthog_person"."properties" -> '$some_prop_1') = '"something_1"' AS "flag_X_condition_0" + SELECT (("posthog_person"."properties" -> '$some_prop_1') = '"something_1"' + AND "posthog_person"."properties" ? '$some_prop_1' + AND NOT (("posthog_person"."properties" -> '$some_prop_1') = 'null')) AS "flag_X_condition_0" FROM "posthog_person" INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") WHERE ("posthog_persondistinctid"."distinct_id" = 'example_id_1' @@ -666,7 +668,9 @@ --- # name: TestDecide.test_flag_with_regular_cohorts.8 ' - SELECT ("posthog_person"."properties" -> '$some_prop_1') = '"something_1"' AS "flag_X_condition_0" + SELECT (("posthog_person"."properties" -> '$some_prop_1') = '"something_1"' + AND "posthog_person"."properties" ? '$some_prop_1' + AND NOT (("posthog_person"."properties" -> '$some_prop_1') = 'null')) AS "flag_X_condition_0" FROM "posthog_person" INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") WHERE ("posthog_persondistinctid"."distinct_id" = 'another_id' diff --git a/posthog/api/test/__snapshots__/test_insight.ambr b/posthog/api/test/__snapshots__/test_insight.ambr index f6a1a0c197b7d..7d0567823c5a4 100644 --- a/posthog/api/test/__snapshots__/test_insight.ambr +++ b/posthog/api/test/__snapshots__/test_insight.ambr @@ -790,7 +790,7 @@ "posthog_insightcachingstate"."created_at", "posthog_insightcachingstate"."updated_at" FROM "posthog_insightcachingstate" - WHERE ("posthog_insightcachingstate"."cache_key" = 'cache_8927b6bdeed5f8af7eeba3673259bf59' + WHERE ("posthog_insightcachingstate"."cache_key" = 'cache_2dfd15f76fb6ac02f0972f7b0203a789' AND "posthog_insightcachingstate"."insight_id" = 2 AND "posthog_insightcachingstate"."team_id" = 2) /*controller='project_insights-list',route='api/projects/%28%3FP%3Cparent_lookup_team_id%3E%5B%5E/.%5D%2B%29/insights/%3F%24'*/ ' From 7271431fbe16529fc9752af52dda05dc55efd817 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 09:57:49 +0100 Subject: [PATCH 090/142] fix webpack build for storybook --- webpack.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index 33228a392c92f..dc958a503b5c2 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -65,7 +65,7 @@ function createEntry(entry) { rules: [ { test: /\.[jt]sx?$/, - exclude: /(node_modules)/, + exclude: /node_modules(?!(\/\.pnpm|)(\/sql-formatter))/, use: { loader: 'babel-loader', }, From 55ba79e8ce9f8b88af6a2477ef52d77709832714 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 10:42:46 +0100 Subject: [PATCH 091/142] asterisks work again --- posthog/hogql/ast.py | 8 +++++--- posthog/hogql/constants.py | 3 --- posthog/hogql/printer.py | 2 +- posthog/hogql/test/test_printer.py | 11 ---------- posthog/hogql/test/test_transforms.py | 6 ++++++ posthog/models/event/query_event_list.py | 26 ++---------------------- 6 files changed, 14 insertions(+), 42 deletions(-) create mode 100644 posthog/hogql/test/test_transforms.py diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 6ed0906143a09..c0b6a75a1bf0d 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -58,9 +58,9 @@ def get_child(self, name: str) -> Symbol: if self.has_child(name): field = self.table.get_field(name) if isinstance(field, Table): - return AsteriskSymbol(table=TableSymbol(table=field)) + return TableSymbol(table=field) return FieldSymbol(name=name, table=self) - raise ValueError(f"Field not found: {name}") + raise ValueError(f'Field "{name}" not found on table {type(self.table).__name__}') class TableAliasSymbol(Symbol): @@ -71,6 +71,8 @@ def has_child(self, name: str) -> bool: return self.table.has_child(name) def get_child(self, name: str) -> Symbol: + if name == "*": + return AsteriskSymbol(table=self) if self.has_child(name): return FieldSymbol(name=name, table=self) return self.table.get_child(name) @@ -104,7 +106,7 @@ class SelectQueryAliasSymbol(Symbol): def get_child(self, name: str) -> Symbol: if self.symbol.has_child(name): return FieldSymbol(name=name, table=self) - raise ValueError(f"Field not found: {name}") + raise ValueError(f"Field {name} not found on query with alias {self.name}") def has_child(self, name: str) -> bool: return self.symbol.has_child(name) diff --git a/posthog/hogql/constants.py b/posthog/hogql/constants.py index 92c88a8c65df4..27e180bec6574 100644 --- a/posthog/hogql/constants.py +++ b/posthog/hogql/constants.py @@ -110,9 +110,6 @@ "distinct_id", "elements_chain", "created_at", - "person.id", - "person.created_at", - "person.properties", ] # Never return more rows than this in top level HogQL SELECT statements diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 28a123bd56015..d49e5798cd98e 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -472,7 +472,7 @@ def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return self._print_identifier(symbol.name) def visit_asterisk_symbol(self, symbol: ast.AsteriskSymbol): - raise ValueError("Unexpected asterisk (*). It's only allowed in a SELECT column.") + raise ValueError("Unexpected ast.AsteriskSymbol. Make sure AsteriskExpander has run on the AST.") def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 46576e453e640..e4ce7eba73019 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -329,17 +329,6 @@ def test_comments(self): context = HogQLContext() self.assertEqual(self._expr("event -- something", context), "event") - def test_special_root_properties(self): - self.assertEqual( - self._expr("*"), - "tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at, person_id, person_created_at, person_properties)", - ) - context = HogQLContext() - self.assertEqual( - self._expr("person", context), - "tuple(person_id, person_created_at, person_properties)", - ) - def test_values(self): context = HogQLContext() self.assertEqual(self._expr("event == 'E'", context), "equals(event, %(hogql_val_0)s)") diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py new file mode 100644 index 0000000000000..be11795007137 --- /dev/null +++ b/posthog/hogql/test/test_transforms.py @@ -0,0 +1,6 @@ +from posthog.test.base import BaseTest + + +class TestTransforms(BaseTest): + def test_asterisk_expander(self): + pass diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index 878bf00e4ed32..2a4c8f5f5b483 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -210,6 +210,8 @@ def run_events_query( for expr in select: hogql_context.found_aggregation = False + if expr == "*": + expr = f'tuple({", ".join(SELECT_STAR_FROM_EVENTS_FIELDS)})' clickhouse_sql = translate_hogql(expr, hogql_context) select_columns.append(clickhouse_sql) if not hogql_context.found_aggregation: @@ -273,13 +275,6 @@ def run_events_query( results[index] = list(result) results[index][star] = convert_star_select_to_dict(result[star]) - # Convert person field from tuple to dict in each result - if "person" in select: - person = select.index("person") - for index, result in enumerate(results): - results[index] = list(result) - results[index][person] = convert_person_select_to_dict(result[person]) - received_extra_row = len(results) == limit # limit was +=1'd above return EventsQueryResponse( @@ -293,23 +288,6 @@ def run_events_query( def convert_star_select_to_dict(select: Tuple[Any]) -> Dict[str, Any]: new_result = dict(zip(SELECT_STAR_FROM_EVENTS_FIELDS, select)) new_result["properties"] = json.loads(new_result["properties"]) - new_result["person"] = { - "id": new_result["person.id"], - "created_at": new_result["person.created_at"], - "properties": json.loads(new_result["person.properties"]), - } - new_result.pop("person.id") - new_result.pop("person.created_at") - new_result.pop("person.properties") if new_result["elements_chain"]: new_result["elements"] = ElementSerializer(chain_to_elements(new_result["elements_chain"]), many=True).data return new_result - - -def convert_person_select_to_dict(select: Tuple[str, str, str, str, str]) -> Dict[str, Any]: - return { - "id": select[1], - "created_at": select[2], - "properties": {"name": select[3], "email": select[4]}, - "distinct_ids": [select[0]], - } From f73998042d36ae5f45a9ee0e3de8f3d8eab60d61 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Feb 2023 09:52:44 +0000 Subject: [PATCH 092/142] Update snapshots --- posthog/api/test/__snapshots__/test_query.ambr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/posthog/api/test/__snapshots__/test_query.ambr b/posthog/api/test/__snapshots__/test_query.ambr index 158a059e51a88..34702611c2ded 100644 --- a/posthog/api/test/__snapshots__/test_query.ambr +++ b/posthog/api/test/__snapshots__/test_query.ambr @@ -348,12 +348,12 @@ # name: TestQuery.test_select_hogql_expressions.1 ' /* user_id:0 request:_snapshot_ */ - SELECT tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at, person_id, person_created_at, person_properties), + SELECT tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at), event FROM events WHERE team_id = 2 AND timestamp < '2020-01-10 12:14:05.000000' - ORDER BY tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at, person_id, person_created_at, person_properties) ASC + ORDER BY tuple(uuid, event, properties, timestamp, team_id, distinct_id, elements_chain, created_at) ASC LIMIT 101 ' --- From 02468b7bd2778166e701c8849c5f8d58eedafff1 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 11:11:23 +0100 Subject: [PATCH 093/142] test splotch desplotcher --- posthog/hogql/ast.py | 4 + posthog/hogql/test/test_transforms.py | 117 +++++++++++++++++++++++++- posthog/hogql/transforms.py | 35 ++++++-- 3 files changed, 146 insertions(+), 10 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index c0b6a75a1bf0d..45c8c71ebe6d4 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -91,6 +91,8 @@ class SelectQuerySymbol(Symbol): anonymous_tables: List["SelectQuerySymbol"] = PydanticField(default_factory=list) def get_child(self, name: str) -> Symbol: + if name == "*": + return AsteriskSymbol(table=self) if name in self.columns: return FieldSymbol(name=name, table=self) raise ValueError(f"Column not found: {name}") @@ -104,6 +106,8 @@ class SelectQueryAliasSymbol(Symbol): symbol: SelectQuerySymbol def get_child(self, name: str) -> Symbol: + if name == "*": + return AsteriskSymbol(table=self) if self.symbol.has_child(name): return FieldSymbol(name=name, table=self) raise ValueError(f"Field {name} not found on query with alias {self.name}") diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py index be11795007137..b32353eba1a37 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/test/test_transforms.py @@ -1,6 +1,119 @@ +from posthog.hogql import ast +from posthog.hogql.database import database +from posthog.hogql.parser import parse_select +from posthog.hogql.resolver import ResolverException, resolve_symbols +from posthog.hogql.transforms import expand_asterisks from posthog.test.base import BaseTest class TestTransforms(BaseTest): - def test_asterisk_expander(self): - pass + def test_asterisk_expander_table(self): + node = parse_select("select * from events") + resolve_symbols(node) + expand_asterisks(node) + events_table_symbol = ast.TableSymbol(table=database.events) + self.assertEqual( + node.select, + [ + ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_symbol)), + ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_symbol)), + ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_symbol)), + ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_symbol)), + ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_symbol)), + ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_symbol)), + ast.Field( + chain=["elements_chain"], symbol=ast.FieldSymbol(name="elements_chain", table=events_table_symbol) + ), + ast.Field(chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_symbol)), + ], + ) + + def test_asterisk_expander_table_alias(self): + node = parse_select("select * from events e") + resolve_symbols(node) + expand_asterisks(node) + events_table_symbol = ast.TableSymbol(table=database.events) + events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + self.assertEqual( + node.select, + [ + ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_alias_symbol)), + ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol)), + ast.Field( + chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_alias_symbol) + ), + ast.Field( + chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) + ), + ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_alias_symbol)), + ast.Field( + chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_alias_symbol) + ), + ast.Field( + chain=["elements_chain"], + symbol=ast.FieldSymbol(name="elements_chain", table=events_table_alias_symbol), + ), + ast.Field( + chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_alias_symbol) + ), + ], + ) + + def test_asterisk_expander_subquery(self): + node = parse_select("select * from (select 1 as a, 2 as b)") + resolve_symbols(node) + expand_asterisks(node) + select_subquery_symbol = ast.SelectQuerySymbol( + aliases={ + "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), + "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), + }, + columns={ + "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), + "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), + }, + tables={}, + anonymous_tables=[], + ) + self.assertEqual( + node.select, + [ + ast.Field(chain=["a"], symbol=ast.FieldSymbol(name="a", table=select_subquery_symbol)), + ast.Field(chain=["b"], symbol=ast.FieldSymbol(name="b", table=select_subquery_symbol)), + ], + ) + + def test_asterisk_expander_subquery_alias(self): + node = parse_select("select x.* from (select 1 as a, 2 as b) x") + resolve_symbols(node) + expand_asterisks(node) + select_subquery_symbol = ast.SelectQueryAliasSymbol( + name="x", + symbol=ast.SelectQuerySymbol( + aliases={ + "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), + "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), + }, + columns={ + "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), + "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), + }, + tables={}, + anonymous_tables=[], + ), + ) + self.assertEqual( + node.select, + [ + ast.Field(chain=["a"], symbol=ast.FieldSymbol(name="a", table=select_subquery_symbol)), + ast.Field(chain=["b"], symbol=ast.FieldSymbol(name="b", table=select_subquery_symbol)), + ], + ) + + def test_asterisk_expander_multiple_table_error(self): + node = parse_select("select * from (select 1 as a, 2 as b) x left join (select 1 as a, 2 as b) y on x.a = y.a") + with self.assertRaises(ResolverException) as e: + resolve_symbols(node) + self.assertEqual( + str(e.exception), "Cannot use '*' without table name when there are multiple tables in the query" + ) diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index b7a30329407b4..b14555b850ba9 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -14,15 +14,34 @@ def visit_select_query(self, node: ast.SelectQuery): for column in node.select: if isinstance(column.symbol, ast.AsteriskSymbol): asterisk = column.symbol - table = asterisk.table - while isinstance(table, ast.TableAliasSymbol): - table = table.table - if isinstance(table, ast.TableSymbol): - database_fields = table.table.get_asterisk() - for key in database_fields.keys(): - columns.append(ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=asterisk.table))) + if isinstance(asterisk.table, ast.TableSymbol) or isinstance(asterisk.table, ast.TableAliasSymbol): + table = asterisk.table + while isinstance(table, ast.TableAliasSymbol): + table = table.table + if isinstance(table, ast.TableSymbol): + database_fields = table.table.get_asterisk() + for key in database_fields.keys(): + columns.append( + ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=asterisk.table)) + ) + else: + raise ValueError("Can't expand asterisk (*) on table") + elif isinstance(asterisk.table, ast.SelectQuerySymbol) or isinstance( + asterisk.table, ast.SelectQueryAliasSymbol + ): + select = asterisk.table + while isinstance(select, ast.SelectQueryAliasSymbol): + select = select.symbol + if isinstance(select, ast.SelectQuerySymbol): + for name in select.columns.keys(): + columns.append( + ast.Field(chain=[name], symbol=ast.FieldSymbol(name=name, table=asterisk.table)) + ) + else: + raise ValueError("Can't expand asterisk (*) on subquery") else: - raise ValueError("Can't expand asterisk (*) on subquery") + raise ValueError(f"Can't expand asterisk (*) on a symbol of type {type(asterisk.table).__name__}") + else: columns.append(column) node.select = columns From 837ff1d7d928e272c134c0e032b982fa3e8a2a8a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 12:45:37 +0100 Subject: [PATCH 094/142] fix test --- posthog/hogql/database.py | 11 ++++++----- posthog/hogql/test/test_transforms.py | 2 -- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 83dbe6ee1e9bb..22a9ad5539b0b 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -135,12 +135,13 @@ class PersonDistinctIdTable(Table): from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table ) - def get_splash(self) -> Dict[str, DatabaseField]: - splash: Dict[str, DatabaseField] = {} - for key, value in super().get_splash().items(): + # Remove the "is_deleted" and "version" columns from "select *" + def get_asterisk(self) -> Dict[str, DatabaseField]: + asterisk: Dict[str, DatabaseField] = {} + for key, value in super().get_asterisk().items(): if key != "is_deleted" and key != "version": - splash[key] = value - return splash + asterisk[key] = value + return asterisk def clickhouse_table(self): return "person_distinct_id2" diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py index ff5354c54319b..912ca16bb4096 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/test/test_transforms.py @@ -19,7 +19,6 @@ def test_asterisk_expander_table(self): ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_symbol)), ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_symbol)), ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_symbol)), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_symbol)), ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_symbol)), ast.Field( chain=["elements_chain"], symbol=ast.FieldSymbol(name="elements_chain", table=events_table_symbol) @@ -45,7 +44,6 @@ def test_asterisk_expander_table_alias(self): ast.Field( chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) ), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_alias_symbol)), ast.Field( chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_alias_symbol) ), From ecdc8ae50e7f54cdbe8724ce5efd29f3f4f375e8 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 14:41:46 +0100 Subject: [PATCH 095/142] field traverser --- posthog/hogql/ast.py | 15 ++++++++++++++- posthog/hogql/database.py | 15 ++++++++++++++- posthog/hogql/hogql.py | 4 ++-- posthog/hogql/printer.py | 5 ++++- posthog/hogql/resolver.py | 15 ++++++++++++--- posthog/hogql/test/test_printer.py | 6 +++--- posthog/hogql/transforms.py | 13 ++++++++----- posthog/hogql/visitor.py | 3 +++ 8 files changed, 60 insertions(+), 16 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 3912941997cd9..b4c44720b8e41 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import DatabaseField, JoinedTable, StringJSONDatabaseField, Table +from posthog.hogql.database import DatabaseField, FieldTraverser, JoinedTable, StringJSONDatabaseField, Table # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -59,6 +59,8 @@ def get_child(self, name: str) -> Symbol: field = self.table.get_field(name) if isinstance(field, JoinedTable): return LazyTableSymbol(table=self, field=name, joined_table=field) + if isinstance(field, FieldTraverser): + return FieldTraverserSymbol(chain=field.chain, symbol=self) return FieldSymbol(name=name, table=self) raise ValueError(f'Field "{name}" not found on table {type(self.table).__name__}') @@ -80,6 +82,8 @@ def get_child(self, name: str) -> Symbol: field = table.table.get_field(name) if isinstance(field, JoinedTable): return LazyTableSymbol(table=self, field=name, joined_table=field) + if isinstance(field, FieldTraverser): + return FieldTraverserSymbol(chain=field.chain, symbol=self) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") @@ -99,6 +103,8 @@ def get_child(self, name: str) -> Symbol: field = self.joined_table.table.get_field(name) if isinstance(field, JoinedTable): return LazyTableSymbol(table=self, field=name, joined_table=field) + if isinstance(field, FieldTraverser): + return FieldTraverserSymbol(chain=field.chain, symbol=self) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") @@ -166,6 +172,11 @@ class AsteriskSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] +class FieldTraverserSymbol(Symbol): + chain: List[str] + symbol: Symbol + + class FieldSymbol(Symbol): name: str table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] @@ -190,6 +201,8 @@ def get_child(self, name: str) -> Symbol: ) if isinstance(database_field, StringJSONDatabaseField): return PropertySymbol(name=name, parent=self) + if isinstance(database_field, FieldTraverser): + return FieldTraverserSymbol(chain=database_field.chain, symbol=self) raise ValueError( f'Can not access property "{name}" on field "{self.name}" of type: {type(database_field).__name__}' ) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 22a9ad5539b0b..99a61278d7472 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -56,7 +56,11 @@ def get_asterisk(self) -> Dict[str, DatabaseField]: pass # skip team_id elif isinstance(database_field, DatabaseField): asterisk[key] = database_field - elif isinstance(database_field, Table) or isinstance(database_field, JoinedTable): + elif ( + isinstance(database_field, Table) + or isinstance(database_field, JoinedTable) + or isinstance(database_field, FieldTraverser) + ): pass # ignore virtual tables for now else: raise ValueError(f"Unknown field type {type(database_field).__name__} for asterisk") @@ -72,6 +76,13 @@ class Config: from_field: str +class FieldTraverser(BaseModel): + class Config: + extra = Extra.forbid + + chain: List[str] + + class PersonsTable(Table): id: StringDatabaseField = StringDatabaseField(name="id") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") @@ -210,6 +221,8 @@ class EventsTable(Table): pdi: JoinedTable = JoinedTable( from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table ) + person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) + person_id: FieldTraverser = FieldTraverser(chain=["pdi", "id"]) def clickhouse_table(self): return "events" diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index f39798ae9347c..67b1887a34b6e 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -18,11 +18,11 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", node = parse_select(query, no_placeholders=True) return print_ast(node, context, dialect, stack=[]) else: + node = parse_expr(query, no_placeholders=True) # Create a fake query that selects from "events". Assume were in its scope when evaluating expressions. select_query = ast.SelectQuery( - select=[], symbol=ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) + select=[node], symbol=ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) ) - node = parse_expr(query, no_placeholders=True) return print_ast(node, context, dialect, stack=[select_query]) except SyntaxError as err: diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 788cf41340463..0866309541a6a 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -42,7 +42,7 @@ def print_ast( # modify the cloned tree as needed if dialect == "clickhouse": expand_asterisks(node) - resolve_lazy_tables(node) + resolve_lazy_tables(node, stack) # TODO: add team_id checks (currently done in the printer) return _Printer(context=context, dialect=dialect, stack=stack or []).visit(node) @@ -471,6 +471,9 @@ def visit_asterisk_symbol(self, symbol: ast.AsteriskSymbol): def visit_lazy_table_symbol(self, symbol: ast.LazyTableSymbol): raise ValueError("Unexpected ast.LazyTableSymbol. Make sure LazyTableResolver has run on the AST.") + def visit_field_traverser_symbol(self, symbol: ast.FieldTraverserSymbol): + raise ValueError("Unexpected ast.FieldTraverserSymbol. This should have been resolved.") + def visit_unknown(self, node: ast.AST): raise ValueError(f"Unknown AST node {type(node).__name__}") diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index bc2910fa20392..e28cda919ffad 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -1,6 +1,7 @@ from typing import List, Optional from posthog.hogql import ast +from posthog.hogql.ast import FieldTraverserSymbol from posthog.hogql.database import database from posthog.hogql.visitor import TraversingVisitor @@ -176,11 +177,19 @@ def visit_field(self, node): # Recursively resolve the rest of the chain until we can point to the deepest node. loop_symbol = symbol - for child_name in node.chain[1:]: - loop_symbol = loop_symbol.get_child(child_name) + chain_to_parse = node.chain[1:] + while True: + if isinstance(loop_symbol, FieldTraverserSymbol): + chain_to_parse = loop_symbol.chain + chain_to_parse + loop_symbol = loop_symbol.symbol + continue + if len(chain_to_parse) == 0: + break + next_chain = chain_to_parse.pop(0) + loop_symbol = loop_symbol.get_child(next_chain) if loop_symbol is None: raise ResolverException( - f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {child_name} on {name}" + f"Cannot resolve symbol {'.'.join(node.chain)}. Unable to resolve {next_chain}." ) node.symbol = loop_symbol diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index e4ce7eba73019..c083466faa2ff 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -74,7 +74,7 @@ def test_fields_and_properties(self): context = HogQLContext() self.assertEqual( self._expr("person.properties.bla", context), - "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) self.assertEqual( context.field_access_logs, @@ -83,7 +83,7 @@ def test_fields_and_properties(self): ["person", "properties", "bla"], "person.properties", "bla", - "replaceRegexpAll(JSONExtractRaw(person_properties, %(hogql_val_0)s), '^\"|\"$', '')", + "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) ], ) @@ -213,7 +213,7 @@ def test_expr_parse_errors(self): self._assert_expr_error( "avg(avg(properties.bla))", "Aggregation 'avg' cannot be nested inside another aggregation 'avg'." ) - self._assert_expr_error("person.chipotle", 'Field "chipotle" not found on table EventsPersonSubTable') + self._assert_expr_error("person.chipotle", "Field not found: chipotle") def test_expr_syntax_errors(self): self._assert_expr_error("(", "line 1, column 1: no viable alternative at input '('") diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index 56f2fcc247453..d11f5afcc26ee 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Callable, Dict, List, Set, Union +from typing import Callable, Dict, List, Optional, Set, Union from posthog.hogql import ast from posthog.hogql.resolver import resolve_symbols @@ -59,14 +59,17 @@ def visit_select_query(self, node: ast.SelectQuery): node.select = columns -def resolve_lazy_tables(node: ast.Expr): - LazyTableResolver().visit(node) +def resolve_lazy_tables(node: ast.Expr, stack: Optional[List[ast.SelectQuery]] = None): + if stack: + # TODO: remove this kludge for old props + LazyTableResolver(stack=stack).visit(stack[-1]) + LazyTableResolver(stack=stack).visit(node) class LazyTableResolver(TraversingVisitor): - def __init__(self): + def __init__(self, stack: Optional[List[ast.SelectQuery]] = None): super().__init__() - self.stack_of_fields: List[List[ast.FieldSymbol]] = [] + self.stack_of_fields: List[List[ast.FieldSymbol]] = [[]] if stack else [] def _get_long_table_name( self, select: ast.SelectQuerySymbol, symbol: Union[ast.TableSymbol, ast.LazyTableSymbol, ast.TableAliasSymbol] diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index 9234e9dcf0064..b72a5529325f1 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -97,6 +97,9 @@ def visit_select_query_symbol(self, node: ast.SelectQuerySymbol): def visit_table_symbol(self, node: ast.TableSymbol): pass + def visit_field_traverser_symbol(self, node: ast.LazyTableSymbol): + self.visit(node.table) + def visit_lazy_table_symbol(self, node: ast.LazyTableSymbol): self.visit(node.table) From e84425613147b7edb897d63c42faa9f602dbb6fd Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 19:07:31 +0100 Subject: [PATCH 096/142] revert tiny changes lost in merge --- posthog/hogql/printer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index e5598d1c6e4be..20930c183a06b 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -86,7 +86,7 @@ def visit_select_query(self, node: ast.SelectQuery): visited_join = self.visit_join_expr(next_join) joined_tables.append(visited_join.printed_sql) - # This is an expression we must add to the SELECT's WHERE clause to limit results. + # This is an expression we must add to the SELECT's WHERE clause to limit results, like the team ID guard. extra_where = visited_join.where if extra_where is None: pass @@ -143,6 +143,7 @@ def visit_select_query(self, node: ast.SelectQuery): # If we are printing a SELECT subquery (not the first AST node we are visiting), wrap it in parentheses. if len(self.stack) > 1: response = f"({response})" + return response def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: From 986aa915bb52f9d23aef4ae11f70e38b256dce0c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 22:45:59 +0100 Subject: [PATCH 097/142] make sure lazy tables also get resolved --- posthog/hogql/hogql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog/hogql/hogql.py b/posthog/hogql/hogql.py index 8c5fc90fb7570..29daddd44f9eb 100644 --- a/posthog/hogql/hogql.py +++ b/posthog/hogql/hogql.py @@ -17,8 +17,8 @@ def translate_hogql(query: str, context: HogQLContext, dialect: Literal["hogql", try: # Create a fake query that selects from "events" to have fields to select from. select_query_symbol = ast.SelectQuerySymbol(tables={"events": ast.TableSymbol(table=database.events)}) - select_query = ast.SelectQuery(select=[], symbol=select_query_symbol) node = parse_expr(query, no_placeholders=True) + select_query = ast.SelectQuery(select=[node], symbol=select_query_symbol) return print_ast(node, context=context, dialect=dialect, stack=[select_query]) except SyntaxError as err: From 98e05c23baf5bb885010cd42bf099e957d744f7c Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:09:27 +0100 Subject: [PATCH 098/142] get rid of field access logs --- posthog/hogql/context.py | 2 - posthog/hogql/printer.py | 47 +------------ posthog/hogql/test/test_printer.py | 106 +---------------------------- posthog/models/property/util.py | 41 ++++++++--- 4 files changed, 35 insertions(+), 161 deletions(-) diff --git a/posthog/hogql/context.py b/posthog/hogql/context.py index df3aabbd97e12..30bc26402f8ff 100644 --- a/posthog/hogql/context.py +++ b/posthog/hogql/context.py @@ -16,8 +16,6 @@ class HogQLContext: # If set, will save string constants to this dict. Inlines strings into the query if None. values: Dict = field(default_factory=dict) - # List of field and property accesses found in the expression - field_access_logs: List[HogQLFieldAccess] = field(default_factory=list) # Did the last calls to translate_hogql since setting these to False contain any of the following found_aggregation: bool = False # Do we need to join the persons table or not diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 58924775749df..4e28acf18ba6e 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -4,8 +4,8 @@ from ee.clickhouse.materialized_columns.columns import TablesWithMaterializedColumns, get_materialized_columns from posthog.hogql import ast from posthog.hogql.constants import CLICKHOUSE_FUNCTIONS, HOGQL_AGGREGATIONS, MAX_SELECT_RETURNED_ROWS -from posthog.hogql.context import HogQLContext, HogQLFieldAccess -from posthog.hogql.database import Table, database +from posthog.hogql.context import HogQLContext +from posthog.hogql.database import Table from posthog.hogql.print_string import print_clickhouse_identifier, print_hogql_identifier from posthog.hogql.resolver import ResolverException, lookup_field_by_name, resolve_symbols from posthog.hogql.transforms import expand_asterisks, resolve_lazy_tables @@ -380,26 +380,6 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if isinstance(symbol.table, ast.TableAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" - # TODO: refactor this legacy logging - if symbol.name != "properties": - real_table = symbol.table - while isinstance(real_table, ast.TableAliasSymbol): - real_table = real_table.table - - access_table = ( - cast(Literal["event"], "event") - if real_table.table == database.events - else cast(Literal["person"], "person") - ) - self.context.field_access_logs.append( - HogQLFieldAccess( - ["person", symbol.name] if access_table == "person" else [symbol.name], - access_table, - symbol.name, - field_sql, - ) - ) - elif isinstance(symbol.table, ast.SelectQuerySymbol) or isinstance(symbol.table, ast.SelectQueryAliasSymbol): field_sql = self._print_identifier(symbol.name) if isinstance(symbol.table, ast.SelectQueryAliasSymbol) or symbol_with_name_in_scope != symbol: @@ -437,29 +417,6 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") - if not field: - pass - elif field.name == "properties": - # TODO: refactor this legacy logging - self.context.field_access_logs.append( - HogQLFieldAccess( - ["properties", symbol.name], - "event.properties", - symbol.name, - property_sql, - ) - ) - elif field.name == "person_properties": - # TODO: refactor this legacy logging - self.context.field_access_logs.append( - HogQLFieldAccess( - ["person", "properties", symbol.name], - "person.properties", - symbol.name, - property_sql, - ) - ) - return property_sql def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index feb6fe7ad9b7f..d1460ef8b30de 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -2,7 +2,7 @@ from django.test.testcases import TestCase -from posthog.hogql.context import HogQLContext, HogQLFieldAccess +from posthog.hogql.context import HogQLContext from posthog.hogql.hogql import translate_hogql from posthog.hogql.parser import parse_select from posthog.hogql.printer import print_ast @@ -59,68 +59,12 @@ def test_fields_and_properties(self): self._expr("properties.$bla", context), "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '')", ) - self.assertEqual( - context.field_access_logs, - [ - HogQLFieldAccess( - ["properties", "$bla"], - "event.properties", - "$bla", - "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '')", - ) - ], - ) context = HogQLContext() self.assertEqual( self._expr("person.properties.bla", context), "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) - self.assertEqual( - context.field_access_logs, - [ - HogQLFieldAccess( - ["person", "properties", "bla"], - "person.properties", - "bla", - "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", - ) - ], - ) - - context = HogQLContext() - self.assertEqual(self._expr("uuid", context), "uuid") - self.assertEqual(context.field_access_logs, [HogQLFieldAccess(["uuid"], "event", "uuid", "uuid")]) - - context = HogQLContext() - self.assertEqual(self._expr("event", context), "event") - self.assertEqual(context.field_access_logs, [HogQLFieldAccess(["event"], "event", "event", "event")]) - - context = HogQLContext() - self.assertEqual(self._expr("timestamp", context), "timestamp") - self.assertEqual( - context.field_access_logs, [HogQLFieldAccess(["timestamp"], "event", "timestamp", "timestamp")] - ) - - context = HogQLContext() - self.assertEqual(self._expr("distinct_id", context), "distinct_id") - self.assertEqual( - context.field_access_logs, [HogQLFieldAccess(["distinct_id"], "event", "distinct_id", "distinct_id")] - ) - - context = HogQLContext() - self.assertEqual(self._expr("person.id", context), "events.person_id") - self.assertEqual( - context.field_access_logs, - [HogQLFieldAccess(["person", "id"], "person", "id", "events.person_id")], - ) - - context = HogQLContext() - self.assertEqual(self._expr("person.created_at", context), "events.person_created_at") - self.assertEqual( - context.field_access_logs, - [HogQLFieldAccess(["person", "created_at"], "person", "created_at", "events.person_created_at")], - ) def test_hogql_properties(self): self.assertEqual( @@ -230,66 +174,18 @@ def test_expr_syntax_errors(self): def test_returned_properties(self): context = HogQLContext() self._expr("avg(properties.prop) + avg(uuid) + event", context) - self.assertEqual( - context.field_access_logs, - [ - HogQLFieldAccess( - ["properties", "prop"], - "event.properties", - "prop", - "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '')", - ), - HogQLFieldAccess(["uuid"], "event", "uuid", "uuid"), - HogQLFieldAccess(["event"], "event", "event", "event"), - ], - ) self.assertEqual(context.found_aggregation, True) context = HogQLContext() self._expr("coalesce(event, properties.event)", context) - self.assertEqual( - context.field_access_logs, - [ - HogQLFieldAccess(["event"], "event", "event", "event"), - HogQLFieldAccess( - ["properties", "event"], - "event.properties", - "event", - "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '')", - ), - ], - ) self.assertEqual(context.found_aggregation, False) context = HogQLContext() self._expr("count() + sum(timestamp)", context) - self.assertEqual( - context.field_access_logs, [HogQLFieldAccess(["timestamp"], "event", "timestamp", "timestamp")] - ) self.assertEqual(context.found_aggregation, True) context = HogQLContext() self._expr("event + avg(event + properties.event) + avg(event + properties.event)", context) - self.assertEqual( - context.field_access_logs, - [ - HogQLFieldAccess(["event"], "event", "event", "event"), - HogQLFieldAccess(["event"], "event", "event", "event"), - HogQLFieldAccess( - ["properties", "event"], - "event.properties", - "event", - "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_0)s), '^\"|\"$', '')", - ), - HogQLFieldAccess(["event"], "event", "event", "event"), - HogQLFieldAccess( - ["properties", "event"], - "event.properties", - "event", - "replaceRegexpAll(JSONExtractRaw(properties, %(hogql_val_1)s), '^\"|\"$', '')", - ), - ], - ) self.assertEqual(context.found_aggregation, True) def test_logic(self): diff --git a/posthog/models/property/util.py b/posthog/models/property/util.py index 1a0b34f5e989a..30cbbbcda78d9 100644 --- a/posthog/models/property/util.py +++ b/posthog/models/property/util.py @@ -19,7 +19,10 @@ from posthog.clickhouse.kafka_engine import trim_quotes_expr from posthog.clickhouse.materialized_columns import TableWithProperties, get_materialized_columns from posthog.constants import PropertyOperatorType -from posthog.hogql.hogql import HogQLContext, translate_hogql +from posthog.hogql import ast +from posthog.hogql.hogql import HogQLContext +from posthog.hogql.parser import parse_expr +from posthog.hogql.visitor import TraversingVisitor from posthog.models.cohort import Cohort from posthog.models.cohort.util import ( format_cohort_subquery, @@ -769,16 +772,36 @@ def build_selector_regex(selector: Selector) -> str: def extract_tables_and_properties(props: List[Property]) -> TCounter[PropertyIdentifier]: counters: List[tuple] = [] + + class PropertyChecker(TraversingVisitor): + def __init__(self): + self.event_properties: List[str] = [] + self.person_properties: List[str] = [] + + def visit_field(self, node: ast.Field): + if len(node.chain) > 1 and node.chain[0] == "properties": + self.event_properties.append(node.chain[1]) + + if len(node.chain) > 2 and node.chain[0] == "person" and node.chain[1] == "properties": + self.person_properties.append(node.chain[2]) + + if ( + len(node.chain) > 3 + and node.chain[0] == "pdi" + and node.chain[1] == "person" + and node.chain[2] == "properties" + ): + self.person_properties.append(node.chain[3]) + for prop in props: if prop.type == "hogql": - context = HogQLContext() - # TODO: Refactor this. Currently it prints and discards a query, just to check the properties. - translate_hogql(prop.key, context) - for field_access in context.field_access_logs: - if field_access.type == "event.properties": - counters.append((field_access.field, "event", None)) - elif field_access.type == "person.properties": - counters.append((field_access.field, "person", None)) + node = parse_expr(prop.key) + property_checker = PropertyChecker() + property_checker.visit(node) + for field in property_checker.event_properties: + counters.append((field, "event", None)) + for field in property_checker.person_properties: + counters.append((field, "person", None)) else: counters.append((prop.key, prop.type, prop.group_type_index)) return Counter(cast(Iterable, counters)) From e7c38ff9e99338f32346b00c1684edd6ca4e5ce3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:09:35 +0100 Subject: [PATCH 099/142] fix bad merge --- .../queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts index 61fdf88dc3045..566c2dfaa51fb 100644 --- a/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts +++ b/frontend/src/queries/nodes/HogQLQuery/hogQLQueryEditorLogic.ts @@ -3,17 +3,6 @@ import { format } from 'sql-formatter' import { HogQLQuery } from '~/queries/schema' import type { hogQLQueryEditorLogicType } from './hogQLQueryEditorLogicType' -import { format } from 'sql-formatter' - -function formatSQL(sql: string): string { - return format(sql, { - language: 'mysql', - tabWidth: 2, - keywordCase: 'preserve', - linesBetweenQueries: 2, - indentStyle: 'tabularRight', - }) -} function formatSQL(sql: string): string { return format(sql, { From a6836f722726543b1272d9e536f877ae59105887 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:22:59 +0100 Subject: [PATCH 100/142] switch to explicit materialised person props --- posthog/hogql/printer.py | 4 ++++ posthog/hogql/test/test_printer.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4e28acf18ba6e..6671d71e4f7e8 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -416,6 +416,10 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") + if field_sql.endswith("__pdi__person.properties"): + materialized_column = self._get_materialized_column("person", symbol.name, "properties") + if materialized_column: + property_sql = self._print_identifier(materialized_column) return property_sql diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index d1460ef8b30de..7071576fa8265 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -127,8 +127,8 @@ def test_materialized_fields_and_properties(self): materialize("events", "$browser%%%#@!@") self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "`mat_$browser_______`") - materialize("events", "$initial_waffle", table_column="person_properties") - self.assertEqual(self._expr("person.properties['$initial_waffle']"), "`mat_pp_$initial_waffle`") + materialize("person", "$initial_waffle") + self.assertEqual(self._expr("person.properties['$initial_waffle']"), "`pmat_$initial_waffle`") def test_methods(self): self.assertEqual(self._expr("count()"), "count(*)") From 7c0ff20ea9ff73f5ab1630de2adf2b1df327baf1 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:23:53 +0100 Subject: [PATCH 101/142] revert --- posthog/hogql/printer.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 6671d71e4f7e8..4e28acf18ba6e 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -416,10 +416,6 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") - if field_sql.endswith("__pdi__person.properties"): - materialized_column = self._get_materialized_column("person", symbol.name, "properties") - if materialized_column: - property_sql = self._print_identifier(materialized_column) return property_sql From aa8eed7e749357bfa003fadc2d1ac34da54139b3 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:24:51 +0100 Subject: [PATCH 102/142] mat column does not make sense in this context on this field --- posthog/hogql/test/test_printer.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 7071576fa8265..0ff9187d8bd30 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -127,9 +127,6 @@ def test_materialized_fields_and_properties(self): materialize("events", "$browser%%%#@!@") self.assertEqual(self._expr("properties['$browser%%%#@!@']"), "`mat_$browser_______`") - materialize("person", "$initial_waffle") - self.assertEqual(self._expr("person.properties['$initial_waffle']"), "`pmat_$initial_waffle`") - def test_methods(self): self.assertEqual(self._expr("count()"), "count(*)") self.assertEqual(self._expr("countDistinct(event)"), "count(distinct event)") From 83107f5a1dd19ba3975c1a6d9dd20954620d78e4 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Tue, 21 Feb 2023 23:35:18 +0100 Subject: [PATCH 103/142] few fixes --- frontend/src/queries/nodes/DataTable/renderColumn.tsx | 2 +- posthog/hogql/database.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index dac4ebe096f96..90f1ebfa3369b 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -92,7 +92,7 @@ export function renderColumn( ) } return - } else if (key.startsWith('person.properties.')) { + } else if (key.startsWith('person.properties.') && isEventsQuery(query.source)) { const eventRecord = record as EventType const propertyKey = key.substring(18) if (setQuery && isEventsQuery(query.source)) { diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 99a61278d7472..d6e59d94fd14c 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -222,7 +222,7 @@ class EventsTable(Table): from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table ) person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) - person_id: FieldTraverser = FieldTraverser(chain=["pdi", "id"]) + person_id: FieldTraverser = FieldTraverser(chain=["pdi", "person_id"]) def clickhouse_table(self): return "events" From a87a7378512c964b7bff6079758f83334d4d37ad Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 00:11:53 +0100 Subject: [PATCH 104/142] indicate legacy queries --- posthog/api/action.py | 2 +- posthog/hogql/context.py | 2 ++ posthog/models/cohort/util.py | 2 +- posthog/models/event/query_event_list.py | 4 ++-- posthog/models/filters/mixins/hogql.py | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/posthog/api/action.py b/posthog/api/action.py index df7df1e10dcce..15f2b3586af81 100644 --- a/posthog/api/action.py +++ b/posthog/api/action.py @@ -236,7 +236,7 @@ def people(self, request: request.Request, *args: Any, **kwargs: Any) -> Respons def count(self, request: request.Request, **kwargs) -> Response: action = self.get_object() # NOTE: never accepts cohort parameters so no need for explicit person_id_joined_alias - hogql_context = HogQLContext() + hogql_context = HogQLContext(part_of_legacy_query=True) query, params = format_action_filter(team_id=action.team_id, action=action, hogql_context=hogql_context) if query == "": return Response({"count": 0}) diff --git a/posthog/hogql/context.py b/posthog/hogql/context.py index 30bc26402f8ff..1b30dbc487e48 100644 --- a/posthog/hogql/context.py +++ b/posthog/hogql/context.py @@ -20,6 +20,8 @@ class HogQLContext: found_aggregation: bool = False # Do we need to join the persons table or not using_person_on_events: bool = True + # swap person property access with "person_props" table in legacy hogql queries + part_of_legacy_query: bool = False # If set, allows printing full SELECT queries in ClickHouse select_team_id: Optional[int] = None # Do we apply a limit of MAX_SELECT_RETURNED_ROWS=65535 to the topmost select query? diff --git a/posthog/models/cohort/util.py b/posthog/models/cohort/util.py index d692c7192a6d4..7af78363c80e5 100644 --- a/posthog/models/cohort/util.py +++ b/posthog/models/cohort/util.py @@ -230,7 +230,7 @@ def insert_static_cohort(person_uuids: List[Optional[uuid.UUID]], cohort_id: int def recalculate_cohortpeople(cohort: Cohort, pending_version: int) -> Optional[int]: - hogql_context = HogQLContext() + hogql_context = HogQLContext(part_of_legacy_query=True) cohort_query, cohort_params = format_person_query(cohort, 0, hogql_context) before_count = get_cohort_size(cohort.pk, cohort.team_id) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index 3c94d5cbd919f..df98fb2af6fca 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -76,7 +76,7 @@ def query_events_list( ) -> List: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext() + hogql_context = HogQLContext(part_of_legacy_query=True) limit += 1 limit_sql = "LIMIT %(limit)s" @@ -145,7 +145,7 @@ def run_events_query( ) -> EventsQueryResponse: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext() + hogql_context = HogQLContext(part_of_legacy_query=True) # adding +1 to the limit to check if there's a "next page" after the requested results limit = min(QUERY_MAXIMUM_LIMIT, QUERY_DEFAULT_LIMIT if query.limit is None else query.limit) + 1 diff --git a/posthog/models/filters/mixins/hogql.py b/posthog/models/filters/mixins/hogql.py index d02570f549487..1cf9490f7500f 100644 --- a/posthog/models/filters/mixins/hogql.py +++ b/posthog/models/filters/mixins/hogql.py @@ -9,7 +9,7 @@ class HogQLParamMixin: @cached_property def hogql_context(self) -> HogQLContext: - context = self.kwargs.get("hogql_context", HogQLContext()) + context = self.kwargs.get("hogql_context", HogQLContext(part_of_legacy_query=True)) if self.kwargs.get("team"): context.using_person_on_events = self.kwargs["team"].person_on_events_querying_enabled return context From 3150c50838ec22fe095c3ca3518e767ca21e3981 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 00:13:58 +0100 Subject: [PATCH 105/142] move the kludge --- posthog/hogql/printer.py | 14 ++++++++------ posthog/hogql/test/test_printer.py | 6 ++++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 4e28acf18ba6e..e3196ea30822d 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -368,12 +368,6 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): field_sql = self._print_identifier(resolved_field.name) - # :KLUDGE: Legacy person properties handling. Assume we're in a context where the tables have been joined, - # and this "person_props" alias is accessible to us. - # if resolved_field == database.events.pdi.table.person.table.properties: - # if not self.context.using_person_on_events: - # field_sql = "person_props" - # If the field is called on a table that has an alias, prepend the table alias. # If there's another field with the same name in the scope that's not this, prepend the full table name. # Note: we don't prepend a table name for the special "person" fields. @@ -385,6 +379,14 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if isinstance(symbol.table, ast.SelectQueryAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" + # :KLUDGE: Legacy person properties handling. Only used within non-hogql queries, such as insights. + if ( + field_sql == "events__pdi__person.properties" + and self.context.part_of_legacy_query + and not self.context.using_person_on_events + ): + field_sql = "person_props" + else: raise ValueError(f"Unknown FieldSymbol table type: {type(symbol.table).__name__}") diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 0ff9187d8bd30..d038127f3b4cb 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -66,6 +66,12 @@ def test_fields_and_properties(self): "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) + context = HogQLContext(part_of_legacy_query=True, using_person_on_events=False) + self.assertEqual( + self._expr("person.properties.bla", context), + "replaceRegexpAll(JSONExtractRaw(person_props, %(hogql_val_0)s), '^\"|\"$', '')", + ) + def test_hogql_properties(self): self.assertEqual( self._expr("event", HogQLContext(), "hogql"), From 5530a5ede0463d109f12eb84657733bd2479e401 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 00:27:51 +0100 Subject: [PATCH 106/142] move the kludge --- posthog/hogql/printer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index e3196ea30822d..b513d37995f41 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -380,12 +380,11 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): field_sql = f"{self.visit(symbol.table)}.{field_sql}" # :KLUDGE: Legacy person properties handling. Only used within non-hogql queries, such as insights. - if ( - field_sql == "events__pdi__person.properties" - and self.context.part_of_legacy_query - and not self.context.using_person_on_events - ): - field_sql = "person_props" + if field_sql == "events__pdi__person.properties" and self.context.part_of_legacy_query: + if self.context.using_person_on_events: + field_sql = "person_properties" + else: + field_sql = "person_props" else: raise ValueError(f"Unknown FieldSymbol table type: {type(symbol.table).__name__}") From 0456203cabd21f9277ac929c2f9ee76e4ebb7a3e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 13:51:09 +0100 Subject: [PATCH 107/142] support nested splotches --- posthog/hogql/test/test_transforms.py | 32 +++++++++++++++++++++++++++ posthog/hogql/transforms.py | 14 +++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py index b32353eba1a37..923023b255b35 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/test/test_transforms.py @@ -110,6 +110,38 @@ def test_asterisk_expander_subquery_alias(self): ], ) + def test_asterisk_expander_from_subquery_table(self): + node = parse_select("select * from (select * from events) x") + resolve_symbols(node) + expand_asterisks(node) + + events_table_symbol = ast.TableSymbol(table=database.events) + events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + self.assertEqual( + node.select, + [ + ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_alias_symbol)), + ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol)), + ast.Field( + chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_alias_symbol) + ), + ast.Field( + chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) + ), + ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_alias_symbol)), + ast.Field( + chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_alias_symbol) + ), + ast.Field( + chain=["elements_chain"], + symbol=ast.FieldSymbol(name="elements_chain", table=events_table_alias_symbol), + ), + ast.Field( + chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_alias_symbol) + ), + ], + ) + def test_asterisk_expander_multiple_table_error(self): node = parse_select("select * from (select 1 as a, 2 as b) x left join (select 1 as a, 2 as b) y on x.a = y.a") with self.assertRaises(ResolverException) as e: diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index b14555b850ba9..b3d8c91ee5cf0 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -10,6 +10,8 @@ def expand_asterisks(node: ast.Expr): class AsteriskExpander(TraversingVisitor): def visit_select_query(self, node: ast.SelectQuery): + super().visit_select_query(node) + columns: List[ast.Expr] = [] for column in node.select: if isinstance(column.symbol, ast.AsteriskSymbol): @@ -21,9 +23,9 @@ def visit_select_query(self, node: ast.SelectQuery): if isinstance(table, ast.TableSymbol): database_fields = table.table.get_asterisk() for key in database_fields.keys(): - columns.append( - ast.Field(chain=[key], symbol=ast.FieldSymbol(name=key, table=asterisk.table)) - ) + symbol = ast.FieldSymbol(name=key, table=asterisk.table) + columns.append(ast.Field(chain=[key], symbol=symbol)) + node.symbol.columns[key] = symbol else: raise ValueError("Can't expand asterisk (*) on table") elif isinstance(asterisk.table, ast.SelectQuerySymbol) or isinstance( @@ -34,9 +36,9 @@ def visit_select_query(self, node: ast.SelectQuery): select = select.symbol if isinstance(select, ast.SelectQuerySymbol): for name in select.columns.keys(): - columns.append( - ast.Field(chain=[name], symbol=ast.FieldSymbol(name=name, table=asterisk.table)) - ) + symbol = ast.FieldSymbol(name=name, table=asterisk.table) + columns.append(ast.Field(chain=[name], symbol=symbol)) + node.symbol.columns[name] = symbol else: raise ValueError("Can't expand asterisk (*) on subquery") else: From 873b54c080779217fdc32810048c13778e3cddfe Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 14:16:36 +0100 Subject: [PATCH 108/142] actually fix the test --- posthog/hogql/test/test_transforms.py | 43 ++++++++++++++++----------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py index 923023b255b35..b1f7864d43f4f 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/test/test_transforms.py @@ -111,34 +111,41 @@ def test_asterisk_expander_subquery_alias(self): ) def test_asterisk_expander_from_subquery_table(self): - node = parse_select("select * from (select * from events) x") + node = parse_select("select * from (select * from events)") resolve_symbols(node) expand_asterisks(node) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + inner_select_symbol = ast.SelectQuerySymbol( + tables={"events": events_table_symbol}, + anonymous_tables=[], + aliases={}, + columns={ + "uuid": ast.FieldSymbol(name="uuid", table=events_table_symbol), + "event": ast.FieldSymbol(name="event", table=events_table_symbol), + "properties": ast.FieldSymbol(name="properties", table=events_table_symbol), + "timestamp": ast.FieldSymbol(name="timestamp", table=events_table_symbol), + "team_id": ast.FieldSymbol(name="team_id", table=events_table_symbol), + "distinct_id": ast.FieldSymbol(name="distinct_id", table=events_table_symbol), + "elements_chain": ast.FieldSymbol(name="elements_chain", table=events_table_symbol), + "created_at": ast.FieldSymbol(name="created_at", table=events_table_symbol), + }, + ) + self.assertEqual( node.select, [ - ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_alias_symbol)), - ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol)), - ast.Field( - chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_alias_symbol) - ), - ast.Field( - chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) - ), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_alias_symbol)), - ast.Field( - chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_alias_symbol) - ), + ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=inner_select_symbol)), + ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=inner_select_symbol)), + ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=inner_select_symbol)), + ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=inner_select_symbol)), + ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=inner_select_symbol)), + ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=inner_select_symbol)), ast.Field( chain=["elements_chain"], - symbol=ast.FieldSymbol(name="elements_chain", table=events_table_alias_symbol), - ), - ast.Field( - chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_alias_symbol) + symbol=ast.FieldSymbol(name="elements_chain", table=inner_select_symbol), ), + ast.Field(chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=inner_select_symbol)), ], ) From 6199e2a54d26e1b9681c5e00612b4ee9f11d1dab Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 14:35:28 +0100 Subject: [PATCH 109/142] fix person properties if part of a legacy insight context --- posthog/hogql/printer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index b513d37995f41..a8651a05f7feb 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -414,6 +414,18 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") + elif ( + self.context.part_of_legacy_query + and isinstance(table, ast.SelectQueryAliasSymbol) + and table.name.endswith("__pdi__person") + ): + # person properties access in a legacy (non hogql) query + materialized_column = self._get_materialized_column("person", symbol.name, "properties") + if materialized_column: + property_sql = self._print_identifier(materialized_column) + else: + field_sql = self.visit(field_symbol) + property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") else: field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") From 10b93394c1b7f7566d1fe16c2f8689b885311647 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 14:44:35 +0100 Subject: [PATCH 110/142] also if PoE is on --- posthog/hogql/printer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index a8651a05f7feb..1429b1917a2f8 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -420,7 +420,10 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): and table.name.endswith("__pdi__person") ): # person properties access in a legacy (non hogql) query - materialized_column = self._get_materialized_column("person", symbol.name, "properties") + if self.context.using_person_on_events: + materialized_column = self._get_materialized_column("events", symbol.name, "person_properties") + else: + materialized_column = self._get_materialized_column("person", symbol.name, "properties") if materialized_column: property_sql = self._print_identifier(materialized_column) else: From 52334cee1ca8c0c45d7d570b652d8c412a50725a Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 14:45:44 +0100 Subject: [PATCH 111/142] not exposing team_id --- posthog/hogql/test/test_transforms.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py index 9680eb979401c..a149744a5c356 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/test/test_transforms.py @@ -123,7 +123,6 @@ def test_asterisk_expander_from_subquery_table(self): "event": ast.FieldSymbol(name="event", table=events_table_symbol), "properties": ast.FieldSymbol(name="properties", table=events_table_symbol), "timestamp": ast.FieldSymbol(name="timestamp", table=events_table_symbol), - "team_id": ast.FieldSymbol(name="team_id", table=events_table_symbol), "distinct_id": ast.FieldSymbol(name="distinct_id", table=events_table_symbol), "elements_chain": ast.FieldSymbol(name="elements_chain", table=events_table_symbol), "created_at": ast.FieldSymbol(name="created_at", table=events_table_symbol), @@ -137,7 +136,6 @@ def test_asterisk_expander_from_subquery_table(self): ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=inner_select_symbol)), ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=inner_select_symbol)), ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=inner_select_symbol)), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=inner_select_symbol)), ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=inner_select_symbol)), ast.Field( chain=["elements_chain"], From 940f294fc526094f58ce4812d139a002104d2f58 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 15:31:39 +0100 Subject: [PATCH 112/142] lazy table tests and split files --- posthog/hogql/transforms/__init__.py | 2 + posthog/hogql/transforms/asterisk.py | 59 ++++++++++++++ .../lazy_tables.py} | 55 ------------- .../test/test_asterisk.py} | 54 +------------ .../hogql/transforms/test/test_lazy_tables.py | 78 +++++++++++++++++++ 5 files changed, 141 insertions(+), 107 deletions(-) create mode 100644 posthog/hogql/transforms/__init__.py create mode 100644 posthog/hogql/transforms/asterisk.py rename posthog/hogql/{transforms.py => transforms/lazy_tables.py} (63%) rename posthog/hogql/{test/test_transforms.py => transforms/test/test_asterisk.py} (77%) create mode 100644 posthog/hogql/transforms/test/test_lazy_tables.py diff --git a/posthog/hogql/transforms/__init__.py b/posthog/hogql/transforms/__init__.py new file mode 100644 index 0000000000000..0b5c7dad0bb25 --- /dev/null +++ b/posthog/hogql/transforms/__init__.py @@ -0,0 +1,2 @@ +from .asterisk import expand_asterisks +from .lazy_tables import resolve_lazy_tables diff --git a/posthog/hogql/transforms/asterisk.py b/posthog/hogql/transforms/asterisk.py new file mode 100644 index 0000000000000..54eebd4086e0c --- /dev/null +++ b/posthog/hogql/transforms/asterisk.py @@ -0,0 +1,59 @@ +from typing import List + +from posthog.hogql import ast +from posthog.hogql.visitor import TraversingVisitor + + +def expand_asterisks(node: ast.Expr): + AsteriskExpander().visit(node) + + +class AsteriskExpander(TraversingVisitor): + def visit_select_query(self, node: ast.SelectQuery): + super().visit_select_query(node) + + columns: List[ast.Expr] = [] + for column in node.select: + if isinstance(column.symbol, ast.AsteriskSymbol): + asterisk = column.symbol + if ( + isinstance(asterisk.table, ast.TableSymbol) + or isinstance(asterisk.table, ast.TableAliasSymbol) + or isinstance(asterisk.table, ast.LazyTableSymbol) + ): + table = asterisk.table + while isinstance(table, ast.TableAliasSymbol): + table = table.table + if isinstance(table, ast.TableSymbol): + database_fields = table.table.get_asterisk() + for key in database_fields.keys(): + symbol = ast.FieldSymbol(name=key, table=asterisk.table) + columns.append(ast.Field(chain=[key], symbol=symbol)) + node.symbol.columns[key] = symbol + elif isinstance(table, ast.LazyTableSymbol): + database_fields = table.joined_table.table.get_asterisk() + for key in database_fields.keys(): + symbol = ast.FieldSymbol(name=key, table=asterisk.table) + columns.append(ast.Field(chain=[key], symbol=symbol)) + node.symbol.columns[key] = symbol + else: + raise ValueError("Can't expand asterisk (*) on table") + elif isinstance(asterisk.table, ast.SelectQuerySymbol) or isinstance( + asterisk.table, ast.SelectQueryAliasSymbol + ): + select = asterisk.table + while isinstance(select, ast.SelectQueryAliasSymbol): + select = select.symbol + if isinstance(select, ast.SelectQuerySymbol): + for name in select.columns.keys(): + symbol = ast.FieldSymbol(name=name, table=asterisk.table) + columns.append(ast.Field(chain=[name], symbol=symbol)) + node.symbol.columns[name] = symbol + else: + raise ValueError("Can't expand asterisk (*) on subquery") + else: + raise ValueError(f"Can't expand asterisk (*) on a symbol of type {type(asterisk.table).__name__}") + + else: + columns.append(column) + node.select = columns diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms/lazy_tables.py similarity index 63% rename from posthog/hogql/transforms.py rename to posthog/hogql/transforms/lazy_tables.py index 0aa96a5c8c48d..838e36ce7c8c9 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -6,61 +6,6 @@ from posthog.hogql.visitor import TraversingVisitor -def expand_asterisks(node: ast.Expr): - AsteriskExpander().visit(node) - - -class AsteriskExpander(TraversingVisitor): - def visit_select_query(self, node: ast.SelectQuery): - super().visit_select_query(node) - - columns: List[ast.Expr] = [] - for column in node.select: - if isinstance(column.symbol, ast.AsteriskSymbol): - asterisk = column.symbol - if ( - isinstance(asterisk.table, ast.TableSymbol) - or isinstance(asterisk.table, ast.TableAliasSymbol) - or isinstance(asterisk.table, ast.LazyTableSymbol) - ): - table = asterisk.table - while isinstance(table, ast.TableAliasSymbol): - table = table.table - if isinstance(table, ast.TableSymbol): - database_fields = table.table.get_asterisk() - for key in database_fields.keys(): - symbol = ast.FieldSymbol(name=key, table=asterisk.table) - columns.append(ast.Field(chain=[key], symbol=symbol)) - node.symbol.columns[key] = symbol - elif isinstance(table, ast.LazyTableSymbol): - database_fields = table.joined_table.table.get_asterisk() - for key in database_fields.keys(): - symbol = ast.FieldSymbol(name=key, table=asterisk.table) - columns.append(ast.Field(chain=[key], symbol=symbol)) - node.symbol.columns[key] = symbol - else: - raise ValueError("Can't expand asterisk (*) on table") - elif isinstance(asterisk.table, ast.SelectQuerySymbol) or isinstance( - asterisk.table, ast.SelectQueryAliasSymbol - ): - select = asterisk.table - while isinstance(select, ast.SelectQueryAliasSymbol): - select = select.symbol - if isinstance(select, ast.SelectQuerySymbol): - for name in select.columns.keys(): - symbol = ast.FieldSymbol(name=name, table=asterisk.table) - columns.append(ast.Field(chain=[name], symbol=symbol)) - node.symbol.columns[name] = symbol - else: - raise ValueError("Can't expand asterisk (*) on subquery") - else: - raise ValueError(f"Can't expand asterisk (*) on a symbol of type {type(asterisk.table).__name__}") - - else: - columns.append(column) - node.select = columns - - def resolve_lazy_tables(node: ast.Expr, stack: Optional[List[ast.SelectQuery]] = None): if stack: # TODO: remove this kludge for old props diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/transforms/test/test_asterisk.py similarity index 77% rename from posthog/hogql/test/test_transforms.py rename to posthog/hogql/transforms/test/test_asterisk.py index a149744a5c356..1504c338d4089 100644 --- a/posthog/hogql/test/test_transforms.py +++ b/posthog/hogql/transforms/test/test_asterisk.py @@ -2,11 +2,11 @@ from posthog.hogql.database import database from posthog.hogql.parser import parse_select from posthog.hogql.resolver import ResolverException, resolve_symbols -from posthog.hogql.transforms import expand_asterisks, resolve_lazy_tables +from posthog.hogql.transforms import expand_asterisks from posthog.test.base import BaseTest -class TestTransforms(BaseTest): +class TestAsteriskExpander(BaseTest): def test_asterisk_expander_table(self): node = parse_select("select * from events") resolve_symbols(node) @@ -152,53 +152,3 @@ def test_asterisk_expander_multiple_table_error(self): self.assertEqual( str(e.exception), "Cannot use '*' without table name when there are multiple tables in the query" ) - - def test_resolve_lazy_tables(self): - expr = parse_select("select event, pdi.person_id from events") - resolve_symbols(expr) - resolve_lazy_tables(expr) - events_table_symbol = ast.TableSymbol(table=database.events) - next_join = database.events.pdi.join_function("events", "events__pdi", ["person_id"]) - # resolve_symbols(next_join, expr.symbol) - - expected = ast.SelectQuery( - select=[ - ast.Field( - chain=["event"], - symbol=ast.FieldSymbol(name="event", table=events_table_symbol), - ), - ast.Field( - chain=["person_id"], - symbol=ast.FieldSymbol( - name="person_id", - table=next_join.table.symbol, - ), - ), - ], - select_from=ast.JoinExpr( - table=ast.Field(chain=["events"], symbol=events_table_symbol), - symbol=events_table_symbol, - next_join=next_join, - ), - symbol=ast.SelectQuerySymbol( - aliases={}, - anonymous_tables=[], - columns={ - "event": ast.FieldSymbol(name="event", table=events_table_symbol), - "person_id": ast.FieldSymbol( - name="person_id", - table=ast.LazyTableSymbol( - table=events_table_symbol, - joined_table=database.events.pdi, - field="pdi", - ), - ), - }, - tables={"events": events_table_symbol}, - ), - ) - self.assertEqual(expr.select, expected.select) - self.assertEqual(expr.select_from, expected.select_from) - self.assertEqual(expr.where, expected.where) - self.assertEqual(expr.symbol, expected.symbol) - self.assertEqual(expr, expected) diff --git a/posthog/hogql/transforms/test/test_lazy_tables.py b/posthog/hogql/transforms/test/test_lazy_tables.py new file mode 100644 index 0000000000000..4136188bd692c --- /dev/null +++ b/posthog/hogql/transforms/test/test_lazy_tables.py @@ -0,0 +1,78 @@ +from posthog.hogql.context import HogQLContext +from posthog.hogql.parser import parse_select +from posthog.hogql.printer import print_ast +from posthog.hogql.resolver import resolve_symbols +from posthog.hogql.transforms import resolve_lazy_tables +from posthog.test.base import BaseTest + + +class TestLazyTables(BaseTest): + def test_resolve_lazy_tables(self): + printed = self._print_select("select event, pdi.person_id from events") + expected = ( + "SELECT event, events__pdi.person_id " + "FROM events " + "INNER JOIN " + "(SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id " + "FROM person_distinct_id2 WHERE equals(team_id, 42) GROUP BY distinct_id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi " + "ON equals(events.distinct_id, events__pdi.distinct_id) " + "WHERE equals(team_id, 42) " + "LIMIT 65535" + ) + self.assertEqual(printed, expected) + + def test_resolve_lazy_tables_traversed_fields(self): + printed = self._print_select("select event, person_id from events") + expected = ( + "SELECT event, events__pdi.person_id " + "FROM events " + "INNER JOIN " + "(SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id " + "FROM person_distinct_id2 WHERE equals(team_id, 42) GROUP BY distinct_id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi " + "ON equals(events.distinct_id, events__pdi.distinct_id) " + "WHERE equals(team_id, 42) " + "LIMIT 65535" + ) + self.assertEqual(printed, expected) + + def test_resolve_lazy_tables_two_levels(self): + printed = self._print_select("select event, pdi.person.id from events") + expected = ( + "SELECT event, events__pdi__person.id " + "FROM events " + "INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id " + "FROM person_distinct_id2 WHERE equals(team_id, 42) GROUP BY distinct_id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi " + "ON equals(events.distinct_id, events__pdi.distinct_id) " + "INNER JOIN (SELECT id FROM person WHERE equals(team_id, 42) GROUP BY id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi__person " + "ON equals(events__pdi.person_id, events__pdi__person.id) " + "WHERE equals(team_id, 42) " + "LIMIT 65535" + ) + self.assertEqual(printed, expected) + + def test_resolve_lazy_tables_two_levels_traversed(self): + printed = self._print_select("select event, person.id from events") + expected = ( + "SELECT event, events__pdi__person.id " + "FROM events " + "INNER JOIN (SELECT argMax(person_distinct_id2.person_id, version) AS person_id, distinct_id " + "FROM person_distinct_id2 WHERE equals(team_id, 42) GROUP BY distinct_id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi " + "ON equals(events.distinct_id, events__pdi.distinct_id) " + "INNER JOIN (SELECT id FROM person WHERE equals(team_id, 42) GROUP BY id " + "HAVING equals(argMax(is_deleted, version), 0)) AS events__pdi__person " + "ON equals(events__pdi.person_id, events__pdi__person.id) " + "WHERE equals(team_id, 42) " + "LIMIT 65535" + ) + self.assertEqual(printed, expected) + + def _print_select(self, select: str): + expr = parse_select(select) + resolve_symbols(expr) + resolve_lazy_tables(expr) + return print_ast(expr, HogQLContext(select_team_id=42), "clickhouse") From 2e7521715f506565defb7360b09291a5cdfc8c41 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 15:39:58 +0100 Subject: [PATCH 113/142] simplify --- frontend/src/queries/examples.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index 58cb7a0018517..ced40ad9bc354 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -310,15 +310,15 @@ const HogQLTable: DataTableNode = { source: { kind: NodeKind.HogQLQuery, query: ` select event, - pdi.person.properties.email, + person.properties.email, properties.$browser, count() from events where timestamp > now () - interval 1 month - and pdi.person.properties.email is not null + and person.properties.email is not null group by event, properties.$browser, - pdi.person.properties.email + person.properties.email order by count() desc`, }, } From ef8d352e438f9953d1aa364dbe4529bd78dbd089 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 15:40:43 +0100 Subject: [PATCH 114/142] example --- frontend/src/queries/examples.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index ced40ad9bc354..3b2d66906e1fc 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -319,7 +319,8 @@ const HogQLTable: DataTableNode = { group by event, properties.$browser, person.properties.email - order by count() desc`, + order by count() desc + limit 100`, }, } From d8a4791a5887484ad16442e13abd4e6f24c1de80 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 16:12:33 +0100 Subject: [PATCH 115/142] feat(hogql): support intervals --- frontend/src/queries/examples.ts | 1 + posthog/hogql/parser.py | 21 ++++++++++++++++++++- posthog/hogql/test/test_parser.py | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/examples.ts b/frontend/src/queries/examples.ts index c53e9ea62a590..ac6161a318209 100644 --- a/frontend/src/queries/examples.ts +++ b/frontend/src/queries/examples.ts @@ -298,6 +298,7 @@ const HogQL: HogQLQuery = { ' properties.$geoip_country_name as `Country Name`,\n' + ' count() as `Event count`\n' + ' from events\n' + + ' where timestamp > now() - interval 1 month\n' + ' group by event,\n' + ' properties.$geoip_country_name\n' + ' order by count() desc\n' + diff --git a/posthog/hogql/parser.py b/posthog/hogql/parser.py index b7be7d161938a..de1eef6975045 100644 --- a/posthog/hogql/parser.py +++ b/posthog/hogql/parser.py @@ -408,7 +408,26 @@ def visitColumnExprPrecedence3(self, ctx: HogQLParser.ColumnExprPrecedence3Conte return ast.CompareOperation(left=self.visit(ctx.left), right=self.visit(ctx.right), op=op) def visitColumnExprInterval(self, ctx: HogQLParser.ColumnExprIntervalContext): - raise NotImplementedError(f"Unsupported node: ColumnExprInterval") + if ctx.interval().SECOND(): + name = "toIntervalSecond" + elif ctx.interval().MINUTE(): + name = "toIntervalMinute" + elif ctx.interval().HOUR(): + name = "toIntervalHour" + elif ctx.interval().DAY(): + name = "toIntervalDay" + elif ctx.interval().WEEK(): + name = "toIntervalWeek" + elif ctx.interval().MONTH(): + name = "toIntervalMonth" + elif ctx.interval().QUARTER(): + name = "toIntervalQuarter" + elif ctx.interval().YEAR(): + name = "toIntervalYear" + else: + raise NotImplementedError(f"Unsupported interval type: {ctx.interval().getText()}") + + return ast.Call(name=name, args=[self.visit(ctx.columnExpr())]) def visitColumnExprIsNull(self, ctx: HogQLParser.ColumnExprIsNullContext): return ast.CompareOperation( diff --git a/posthog/hogql/test/test_parser.py b/posthog/hogql/test/test_parser.py index 8c51fc282e1f7..2a1455d5538d6 100644 --- a/posthog/hogql/test/test_parser.py +++ b/posthog/hogql/test/test_parser.py @@ -355,6 +355,24 @@ def test_placeholders(self): ), ) + def test_intervals(self): + self.assertEqual( + parse_expr("interval 1 month"), + ast.Call(name="toIntervalMonth", args=[ast.Constant(value=1)]), + ) + self.assertEqual( + parse_expr("now() - interval 1 week"), + ast.BinaryOperation( + op=ast.BinaryOperationType.Sub, + left=ast.Call(name="now", args=[]), + right=ast.Call(name="toIntervalWeek", args=[ast.Constant(value=1)]), + ), + ) + self.assertEqual( + parse_expr("interval event year"), + ast.Call(name="toIntervalYear", args=[ast.Field(chain=["event"])]), + ) + def test_select_columns(self): self.assertEqual(parse_select("select 1"), ast.SelectQuery(select=[ast.Constant(value=1)])) self.assertEqual( From bde6e9f40f7cf1087ac3e78bb7e7b326fb8efbdf Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 16:28:32 +0100 Subject: [PATCH 116/142] rename --- posthog/api/action.py | 2 +- posthog/hogql/context.py | 10 +++++----- posthog/hogql/printer.py | 4 ++-- posthog/hogql/test/test_printer.py | 2 +- posthog/models/cohort/util.py | 2 +- posthog/models/event/query_event_list.py | 4 ++-- posthog/models/filters/mixins/hogql.py | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/posthog/api/action.py b/posthog/api/action.py index 15f2b3586af81..10b7243efcee2 100644 --- a/posthog/api/action.py +++ b/posthog/api/action.py @@ -236,7 +236,7 @@ def people(self, request: request.Request, *args: Any, **kwargs: Any) -> Respons def count(self, request: request.Request, **kwargs) -> Response: action = self.get_object() # NOTE: never accepts cohort parameters so no need for explicit person_id_joined_alias - hogql_context = HogQLContext(part_of_legacy_query=True) + hogql_context = HogQLContext(legacy_person_property_handling=True) query, params = format_action_filter(team_id=action.team_id, action=action, hogql_context=hogql_context) if query == "": return Response({"count": 0}) diff --git a/posthog/hogql/context.py b/posthog/hogql/context.py index 1b30dbc487e48..6ce3ce85f34f3 100644 --- a/posthog/hogql/context.py +++ b/posthog/hogql/context.py @@ -16,13 +16,13 @@ class HogQLContext: # If set, will save string constants to this dict. Inlines strings into the query if None. values: Dict = field(default_factory=dict) - # Did the last calls to translate_hogql since setting these to False contain any of the following - found_aggregation: bool = False - # Do we need to join the persons table or not + # Are we small part of a non-HogQL query? If so, use custom syntax for accessed person properties. + legacy_person_property_handling: bool = False + # Do we need to join the persons table or not. Has effect if legacy_person_property_handling = True using_person_on_events: bool = True - # swap person property access with "person_props" table in legacy hogql queries - part_of_legacy_query: bool = False # If set, allows printing full SELECT queries in ClickHouse select_team_id: Optional[int] = None # Do we apply a limit of MAX_SELECT_RETURNED_ROWS=65535 to the topmost select query? limit_top_select: bool = True + # To be removed. Did the last calls to translate_hogql since setting this to False contain an aggregation? + found_aggregation: bool = False diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 1429b1917a2f8..a3d89f952ab86 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -380,7 +380,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): field_sql = f"{self.visit(symbol.table)}.{field_sql}" # :KLUDGE: Legacy person properties handling. Only used within non-hogql queries, such as insights. - if field_sql == "events__pdi__person.properties" and self.context.part_of_legacy_query: + if field_sql == "events__pdi__person.properties" and self.context.legacy_person_property_handling: if self.context.using_person_on_events: field_sql = "person_properties" else: @@ -415,7 +415,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") elif ( - self.context.part_of_legacy_query + self.context.legacy_person_property_handling and isinstance(table, ast.SelectQueryAliasSymbol) and table.name.endswith("__pdi__person") ): diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index d038127f3b4cb..06e47992e20b3 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -66,7 +66,7 @@ def test_fields_and_properties(self): "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) - context = HogQLContext(part_of_legacy_query=True, using_person_on_events=False) + context = HogQLContext(legacy_person_property_handling=True, using_person_on_events=False) self.assertEqual( self._expr("person.properties.bla", context), "replaceRegexpAll(JSONExtractRaw(person_props, %(hogql_val_0)s), '^\"|\"$', '')", diff --git a/posthog/models/cohort/util.py b/posthog/models/cohort/util.py index 7af78363c80e5..2accb1452a9d6 100644 --- a/posthog/models/cohort/util.py +++ b/posthog/models/cohort/util.py @@ -230,7 +230,7 @@ def insert_static_cohort(person_uuids: List[Optional[uuid.UUID]], cohort_id: int def recalculate_cohortpeople(cohort: Cohort, pending_version: int) -> Optional[int]: - hogql_context = HogQLContext(part_of_legacy_query=True) + hogql_context = HogQLContext(legacy_person_property_handling=True) cohort_query, cohort_params = format_person_query(cohort, 0, hogql_context) before_count = get_cohort_size(cohort.pk, cohort.team_id) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index df98fb2af6fca..e36ce15879617 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -76,7 +76,7 @@ def query_events_list( ) -> List: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext(part_of_legacy_query=True) + hogql_context = HogQLContext(legacy_person_property_handling=True) limit += 1 limit_sql = "LIMIT %(limit)s" @@ -145,7 +145,7 @@ def run_events_query( ) -> EventsQueryResponse: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext(part_of_legacy_query=True) + hogql_context = HogQLContext(legacy_person_property_handling=True) # adding +1 to the limit to check if there's a "next page" after the requested results limit = min(QUERY_MAXIMUM_LIMIT, QUERY_DEFAULT_LIMIT if query.limit is None else query.limit) + 1 diff --git a/posthog/models/filters/mixins/hogql.py b/posthog/models/filters/mixins/hogql.py index 1cf9490f7500f..cac020aa15cb2 100644 --- a/posthog/models/filters/mixins/hogql.py +++ b/posthog/models/filters/mixins/hogql.py @@ -9,7 +9,7 @@ class HogQLParamMixin: @cached_property def hogql_context(self) -> HogQLContext: - context = self.kwargs.get("hogql_context", HogQLContext(part_of_legacy_query=True)) + context = self.kwargs.get("hogql_context", HogQLContext(legacy_person_property_handling=True)) if self.kwargs.get("team"): context.using_person_on_events = self.kwargs["team"].person_on_events_querying_enabled return context From f8669d4b0a5887ed8d57bc49838e2e47d3280502 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 16:29:28 +0100 Subject: [PATCH 117/142] this file was split in two --- posthog/hogql/test/test_transforms.py | 158 -------------------------- 1 file changed, 158 deletions(-) delete mode 100644 posthog/hogql/test/test_transforms.py diff --git a/posthog/hogql/test/test_transforms.py b/posthog/hogql/test/test_transforms.py deleted file mode 100644 index b1f7864d43f4f..0000000000000 --- a/posthog/hogql/test/test_transforms.py +++ /dev/null @@ -1,158 +0,0 @@ -from posthog.hogql import ast -from posthog.hogql.database import database -from posthog.hogql.parser import parse_select -from posthog.hogql.resolver import ResolverException, resolve_symbols -from posthog.hogql.transforms import expand_asterisks -from posthog.test.base import BaseTest - - -class TestTransforms(BaseTest): - def test_asterisk_expander_table(self): - node = parse_select("select * from events") - resolve_symbols(node) - expand_asterisks(node) - events_table_symbol = ast.TableSymbol(table=database.events) - self.assertEqual( - node.select, - [ - ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_symbol)), - ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_symbol)), - ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_symbol)), - ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_symbol)), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_symbol)), - ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_symbol)), - ast.Field( - chain=["elements_chain"], symbol=ast.FieldSymbol(name="elements_chain", table=events_table_symbol) - ), - ast.Field(chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_symbol)), - ], - ) - - def test_asterisk_expander_table_alias(self): - node = parse_select("select * from events e") - resolve_symbols(node) - expand_asterisks(node) - events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") - self.assertEqual( - node.select, - [ - ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=events_table_alias_symbol)), - ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=events_table_alias_symbol)), - ast.Field( - chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=events_table_alias_symbol) - ), - ast.Field( - chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) - ), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=events_table_alias_symbol)), - ast.Field( - chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=events_table_alias_symbol) - ), - ast.Field( - chain=["elements_chain"], - symbol=ast.FieldSymbol(name="elements_chain", table=events_table_alias_symbol), - ), - ast.Field( - chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=events_table_alias_symbol) - ), - ], - ) - - def test_asterisk_expander_subquery(self): - node = parse_select("select * from (select 1 as a, 2 as b)") - resolve_symbols(node) - expand_asterisks(node) - select_subquery_symbol = ast.SelectQuerySymbol( - aliases={ - "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), - "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), - }, - columns={ - "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), - "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), - }, - tables={}, - anonymous_tables=[], - ) - self.assertEqual( - node.select, - [ - ast.Field(chain=["a"], symbol=ast.FieldSymbol(name="a", table=select_subquery_symbol)), - ast.Field(chain=["b"], symbol=ast.FieldSymbol(name="b", table=select_subquery_symbol)), - ], - ) - - def test_asterisk_expander_subquery_alias(self): - node = parse_select("select x.* from (select 1 as a, 2 as b) x") - resolve_symbols(node) - expand_asterisks(node) - select_subquery_symbol = ast.SelectQueryAliasSymbol( - name="x", - symbol=ast.SelectQuerySymbol( - aliases={ - "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), - "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), - }, - columns={ - "a": ast.FieldAliasSymbol(name="a", symbol=ast.ConstantSymbol(value=1)), - "b": ast.FieldAliasSymbol(name="b", symbol=ast.ConstantSymbol(value=2)), - }, - tables={}, - anonymous_tables=[], - ), - ) - self.assertEqual( - node.select, - [ - ast.Field(chain=["a"], symbol=ast.FieldSymbol(name="a", table=select_subquery_symbol)), - ast.Field(chain=["b"], symbol=ast.FieldSymbol(name="b", table=select_subquery_symbol)), - ], - ) - - def test_asterisk_expander_from_subquery_table(self): - node = parse_select("select * from (select * from events)") - resolve_symbols(node) - expand_asterisks(node) - - events_table_symbol = ast.TableSymbol(table=database.events) - inner_select_symbol = ast.SelectQuerySymbol( - tables={"events": events_table_symbol}, - anonymous_tables=[], - aliases={}, - columns={ - "uuid": ast.FieldSymbol(name="uuid", table=events_table_symbol), - "event": ast.FieldSymbol(name="event", table=events_table_symbol), - "properties": ast.FieldSymbol(name="properties", table=events_table_symbol), - "timestamp": ast.FieldSymbol(name="timestamp", table=events_table_symbol), - "team_id": ast.FieldSymbol(name="team_id", table=events_table_symbol), - "distinct_id": ast.FieldSymbol(name="distinct_id", table=events_table_symbol), - "elements_chain": ast.FieldSymbol(name="elements_chain", table=events_table_symbol), - "created_at": ast.FieldSymbol(name="created_at", table=events_table_symbol), - }, - ) - - self.assertEqual( - node.select, - [ - ast.Field(chain=["uuid"], symbol=ast.FieldSymbol(name="uuid", table=inner_select_symbol)), - ast.Field(chain=["event"], symbol=ast.FieldSymbol(name="event", table=inner_select_symbol)), - ast.Field(chain=["properties"], symbol=ast.FieldSymbol(name="properties", table=inner_select_symbol)), - ast.Field(chain=["timestamp"], symbol=ast.FieldSymbol(name="timestamp", table=inner_select_symbol)), - ast.Field(chain=["team_id"], symbol=ast.FieldSymbol(name="team_id", table=inner_select_symbol)), - ast.Field(chain=["distinct_id"], symbol=ast.FieldSymbol(name="distinct_id", table=inner_select_symbol)), - ast.Field( - chain=["elements_chain"], - symbol=ast.FieldSymbol(name="elements_chain", table=inner_select_symbol), - ), - ast.Field(chain=["created_at"], symbol=ast.FieldSymbol(name="created_at", table=inner_select_symbol)), - ], - ) - - def test_asterisk_expander_multiple_table_error(self): - node = parse_select("select * from (select 1 as a, 2 as b) x left join (select 1 as a, 2 as b) y on x.a = y.a") - with self.assertRaises(ResolverException) as e: - resolve_symbols(node) - self.assertEqual( - str(e.exception), "Cannot use '*' without table name when there are multiple tables in the query" - ) From 6e3844bcc90d3671c821c7cc3c39e28e97bd7189 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 16:30:13 +0100 Subject: [PATCH 118/142] not adding this yet --- posthog/hogql/database.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index d6e59d94fd14c..8563dd7681b1d 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -197,17 +197,6 @@ def join_with_max_person_distinct_id_table(from_table: str, to_table: str, reque ) -class GroupsTable(Table): - group_type_index: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - group_key: StringDatabaseField = StringDatabaseField(name="group_key") - created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - team_id: IntegerDatabaseField = IntegerDatabaseField(name="team_id") - group_properties: StringJSONDatabaseField = StringJSONDatabaseField(name="group_properties") - - def clickhouse_table(self): - return "groups" - - class EventsTable(Table): uuid: StringDatabaseField = StringDatabaseField(name="uuid") event: StringDatabaseField = StringDatabaseField(name="event") @@ -263,7 +252,6 @@ class Config: persons: PersonsTable = PersonsTable() person_distinct_ids: PersonDistinctIdTable = PersonDistinctIdTable() session_recording_events: SessionRecordingEvents = SessionRecordingEvents() - groups: GroupsTable = GroupsTable() def has_table(self, table_name: str) -> bool: return hasattr(self, table_name) From 02602c21663658c8677e444560200ccd6af7c7bb Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 16:31:40 +0100 Subject: [PATCH 119/142] klean kludge --- posthog/hogql/printer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index a3d89f952ab86..1bc213823d4fa 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -379,8 +379,8 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): if isinstance(symbol.table, ast.SelectQueryAliasSymbol) or symbol_with_name_in_scope != symbol: field_sql = f"{self.visit(symbol.table)}.{field_sql}" - # :KLUDGE: Legacy person properties handling. Only used within non-hogql queries, such as insights. - if field_sql == "events__pdi__person.properties" and self.context.legacy_person_property_handling: + # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. + if self.context.legacy_person_property_handling and field_sql == "events__pdi__person.properties": if self.context.using_person_on_events: field_sql = "person_properties" else: @@ -417,9 +417,9 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): elif ( self.context.legacy_person_property_handling and isinstance(table, ast.SelectQueryAliasSymbol) - and table.name.endswith("__pdi__person") + and table.name == "events__pdi__person" ): - # person properties access in a legacy (non hogql) query + # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. if self.context.using_person_on_events: materialized_column = self._get_materialized_column("events", symbol.name, "person_properties") else: From d3bb2cd9fb8f0f671276cfacf51c2dddc376d44e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 20:26:36 +0100 Subject: [PATCH 120/142] explicitly no funny business with the title --- frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx index c16df2ff7ff88..124e6e5c4eca5 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx @@ -1,7 +1,7 @@ import { PropertyFilterType } from '~/types' import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo' import { QueryContext, DataTableNode } from '~/queries/schema' -import { isEventsQuery } from '~/queries/utils' +import { isEventsQuery, isHogQLQuery } from '~/queries/utils' import { extractExpressionComment } from '~/queries/nodes/DataTable/utils' import { SortingIndicator } from 'lib/lemon-ui/LemonTable/sorting' @@ -14,7 +14,9 @@ export function renderColumnMeta(key: string, query: DataTableNode, context?: Qu let width: number | undefined let title: JSX.Element | string | undefined - if (key === 'timestamp') { + if (isHogQLQuery(query.source)) { + title = key + } else if (key === 'timestamp') { title = 'Time' } else if (key === 'created_at') { title = 'Created at' From b40c41f1e8502c1b88c95d256a9403d033c5f21f Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 20:43:00 +0100 Subject: [PATCH 121/142] best guess JSON parsing --- .../src/queries/nodes/DataTable/renderColumn.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 90f1ebfa3369b..0ce659cf83441 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -7,7 +7,7 @@ import { Property } from 'lib/components/Property' import { urls } from 'scenes/urls' import { PersonHeader } from 'scenes/persons/PersonHeader' import { DataTableNode, HasPropertiesNode, QueryContext } from '~/queries/schema' -import { isEventsQuery, isPersonsNode } from '~/queries/utils' +import { isEventsQuery, isHogQLQuery, isPersonsNode } from '~/queries/utils' import { combineUrl, router } from 'kea-router' import { CopyToClipboardInline } from 'lib/components/CopyToClipboard' import { DeletePersonButton } from '~/queries/nodes/PersonsNode/DeletePersonButton' @@ -28,6 +28,20 @@ export function renderColumn( return } else if (value === errorColumn) { return Error + } else if (isHogQLQuery(query.source)) { + if (typeof value === 'string') { + try { + if ((value.startsWith('{') && value.endsWith('}')) || (value.startsWith('[') && value.endsWith(']'))) { + return + } + } catch (e) {} + + if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}/)) { + return + } + return + } + return } else if (key === 'event' && isEventsQuery(query.source)) { const resultRow = record as any[] const eventRecord = query.source.select.includes('*') ? resultRow[query.source.select.indexOf('*')] : null From 2eddaae9f6f4bdd29344fa38ccb48c8b519f8e21 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 20:46:02 +0100 Subject: [PATCH 122/142] remove funny changes --- frontend/src/queries/nodes/DataTable/renderColumn.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumn.tsx b/frontend/src/queries/nodes/DataTable/renderColumn.tsx index 0ce659cf83441..bea6ebc2f204a 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumn.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumn.tsx @@ -35,11 +35,9 @@ export function renderColumn( return } } catch (e) {} - if (value.match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}/)) { return } - return } return } else if (key === 'event' && isEventsQuery(query.source)) { @@ -106,7 +104,7 @@ export function renderColumn( ) } return - } else if (key.startsWith('person.properties.') && isEventsQuery(query.source)) { + } else if (key.startsWith('person.properties.')) { const eventRecord = record as EventType const propertyKey = key.substring(18) if (setQuery && isEventsQuery(query.source)) { From 057e65412c75e6706d95db1b499c9e5c2fc47aaa Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 20:46:22 +0100 Subject: [PATCH 123/142] remove funny changes --- frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx index 124e6e5c4eca5..ea391272f4a61 100644 --- a/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx +++ b/frontend/src/queries/nodes/DataTable/renderColumnMeta.tsx @@ -19,7 +19,7 @@ export function renderColumnMeta(key: string, query: DataTableNode, context?: Qu } else if (key === 'timestamp') { title = 'Time' } else if (key === 'created_at') { - title = 'Created at' + title = 'First seen' } else if (key === 'event') { title = 'Event' } else if (key === 'person') { From b45811f9dd57892adb4f8e8ca512a3ac3bf22c11 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 21:23:43 +0100 Subject: [PATCH 124/142] rename joined table to lazy table --- posthog/hogql/ast.py | 12 ++++++------ posthog/hogql/database.py | 12 +++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index e3080651e76fa..8345296eed418 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import DatabaseField, FieldTraverser, JoinedTable, StringJSONDatabaseField, Table +from posthog.hogql.database import DatabaseField, FieldTraverser, LazyTable, StringJSONDatabaseField, Table # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -57,7 +57,7 @@ def get_child(self, name: str) -> Symbol: return AsteriskSymbol(table=self) if self.has_child(name): field = self.table.get_field(name) - if isinstance(field, JoinedTable): + if isinstance(field, LazyTable): return LazyTableSymbol(table=self, field=name, joined_table=field) if isinstance(field, FieldTraverser): return FieldTraverserSymbol(chain=field.chain, symbol=self) @@ -80,7 +80,7 @@ def get_child(self, name: str) -> Symbol: while isinstance(table, TableAliasSymbol): table = table.table field = table.table.get_field(name) - if isinstance(field, JoinedTable): + if isinstance(field, LazyTable): return LazyTableSymbol(table=self, field=name, joined_table=field) if isinstance(field, FieldTraverser): return FieldTraverserSymbol(chain=field.chain, symbol=self) @@ -91,7 +91,7 @@ def get_child(self, name: str) -> Symbol: class LazyTableSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol, "LazyTableSymbol"] field: str - joined_table: JoinedTable + joined_table: LazyTable def has_child(self, name: str) -> bool: return self.joined_table.table.has_field(name) @@ -101,7 +101,7 @@ def get_child(self, name: str) -> Symbol: return AsteriskSymbol(table=self) if self.has_child(name): field = self.joined_table.table.get_field(name) - if isinstance(field, JoinedTable): + if isinstance(field, LazyTable): return LazyTableSymbol(table=self, field=name, joined_table=field) if isinstance(field, FieldTraverser): return FieldTraverserSymbol(chain=field.chain, symbol=self) @@ -195,7 +195,7 @@ def get_child(self, name: str) -> Symbol: database_field = self.resolve_database_field() if database_field is None: raise ValueError(f'Can not access property "{name}" on field "{self.name}".') - if isinstance(database_field, JoinedTable): + if isinstance(database_field, LazyTable): return FieldSymbol( name=name, table=LazyTableSymbol(table=self.table, field=name, joined_table=database_field) ) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 8563dd7681b1d..2b2ad4329eccf 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -58,7 +58,7 @@ def get_asterisk(self) -> Dict[str, DatabaseField]: asterisk[key] = database_field elif ( isinstance(database_field, Table) - or isinstance(database_field, JoinedTable) + or isinstance(database_field, LazyTable) or isinstance(database_field, FieldTraverser) ): pass # ignore virtual tables for now @@ -67,7 +67,7 @@ def get_asterisk(self) -> Dict[str, DatabaseField]: return asterisk -class JoinedTable(BaseModel): +class LazyTable(BaseModel): class Config: extra = Extra.forbid @@ -142,9 +142,7 @@ class PersonDistinctIdTable(Table): is_deleted: BooleanDatabaseField = BooleanDatabaseField(name="is_deleted") version: IntegerDatabaseField = IntegerDatabaseField(name="version") - person: JoinedTable = JoinedTable( - from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table - ) + person: LazyTable = LazyTable(from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table) # Remove the "is_deleted" and "version" columns from "select *" def get_asterisk(self) -> Dict[str, DatabaseField]: @@ -207,7 +205,7 @@ class EventsTable(Table): elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") - pdi: JoinedTable = JoinedTable( + pdi: LazyTable = LazyTable( from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table ) person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) @@ -235,7 +233,7 @@ class SessionRecordingEvents(Table): last_event_timestamp: DateTimeDatabaseField = DateTimeDatabaseField(name="last_event_timestamp") urls: StringDatabaseField = StringDatabaseField(name="urls", array=True) - pdi: JoinedTable = JoinedTable( + pdi: LazyTable = LazyTable( from_field="distinct_id", table=PersonDistinctIdTable(), join_function=join_with_max_person_distinct_id_table ) From 030c1dc815ec0a3ef6389ba91b8d7c077531ac0d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 21:46:27 +0100 Subject: [PATCH 125/142] refactor --- posthog/hogql/ast.py | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 8345296eed418..d5df22cd58840 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -57,11 +57,7 @@ def get_child(self, name: str) -> Symbol: return AsteriskSymbol(table=self) if self.has_child(name): field = self.table.get_field(name) - if isinstance(field, LazyTable): - return LazyTableSymbol(table=self, field=name, joined_table=field) - if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, symbol=self) - return FieldSymbol(name=name, table=self) + return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f'Field "{name}" not found on table {type(self.table).__name__}') @@ -80,11 +76,7 @@ def get_child(self, name: str) -> Symbol: while isinstance(table, TableAliasSymbol): table = table.table field = table.table.get_field(name) - if isinstance(field, LazyTable): - return LazyTableSymbol(table=self, field=name, joined_table=field) - if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, symbol=self) - return FieldSymbol(name=name, table=self) + return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f"Field not found: {name}") @@ -101,14 +93,20 @@ def get_child(self, name: str) -> Symbol: return AsteriskSymbol(table=self) if self.has_child(name): field = self.joined_table.table.get_field(name) - if isinstance(field, LazyTable): - return LazyTableSymbol(table=self, field=name, joined_table=field) - if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, symbol=self) - return FieldSymbol(name=name, table=self) + return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f"Field not found: {name}") +def database_field_to_symbol( + field: BaseModel, name: str, table_symbol: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol] +) -> Symbol: + if isinstance(field, LazyTable): + return LazyTableSymbol(table=table_symbol, field=name, joined_table=field) + if isinstance(field, FieldTraverser): + return FieldTraverserSymbol(chain=field.chain, symbol=table_symbol) + return FieldSymbol(name=name, table=table_symbol) + + class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope aliases: Dict[str, FieldAliasSymbol] = PydanticField(default_factory=dict) @@ -181,20 +179,27 @@ class FieldSymbol(Symbol): name: str table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] - def resolve_database_field(self) -> Optional[Union[DatabaseField, Table]]: + def resolve_database_table(self) -> Optional[Table]: table_symbol = self.table if isinstance(table_symbol, LazyTableSymbol): - return table_symbol.joined_table.table.get_field(self.name) + return table_symbol.joined_table.table while isinstance(table_symbol, TableAliasSymbol): table_symbol = table_symbol.table if isinstance(table_symbol, TableSymbol): - return table_symbol.table.get_field(self.name) + return table_symbol.table + return None + + def resolve_database_field(self) -> Optional[DatabaseField]: + table = self.resolve_database_table() + if table is not None: + return table.get_field(self.name) return None def get_child(self, name: str) -> Symbol: database_field = self.resolve_database_field() if database_field is None: raise ValueError(f'Can not access property "{name}" on field "{self.name}".') + if isinstance(database_field, LazyTable): return FieldSymbol( name=name, table=LazyTableSymbol(table=self.table, field=name, joined_table=database_field) From d796232a20068598bbbb20c81dc3b0c10234ea80 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 21:54:35 +0100 Subject: [PATCH 126/142] rename field for clarity --- posthog/hogql/ast.py | 14 +++++++------- posthog/hogql/printer.py | 6 +++--- posthog/hogql/resolver.py | 2 +- posthog/hogql/test/test_resolver.py | 8 ++++---- posthog/hogql/transforms.py | 2 +- posthog/hogql/transforms/asterisk.py | 2 +- posthog/hogql/transforms/test/test_asterisk.py | 2 +- posthog/hogql/visitor.py | 2 +- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index d5df22cd58840..9ae48a3fc6305 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -63,19 +63,19 @@ def get_child(self, name: str) -> Symbol: class TableAliasSymbol(Symbol): name: str - table: TableSymbol + table_symbol: TableSymbol def has_child(self, name: str) -> bool: - return self.table.has_child(name) + return self.table_symbol.has_child(name) def get_child(self, name: str) -> Symbol: if name == "*": return AsteriskSymbol(table=self) if self.has_child(name): - table: Union[TableSymbol, TableAliasSymbol] = self - while isinstance(table, TableAliasSymbol): - table = table.table - field = table.table.get_field(name) + table_symbol: Union[TableSymbol, TableAliasSymbol] = self + while isinstance(table_symbol, TableAliasSymbol): + table_symbol = table_symbol.table_symbol + field = table_symbol.table.get_field(name) return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f"Field not found: {name}") @@ -184,7 +184,7 @@ def resolve_database_table(self) -> Optional[Table]: if isinstance(table_symbol, LazyTableSymbol): return table_symbol.joined_table.table while isinstance(table_symbol, TableAliasSymbol): - table_symbol = table_symbol.table + table_symbol = table_symbol.table_symbol if isinstance(table_symbol, TableSymbol): return table_symbol.table return None diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index 1bc213823d4fa..a595aecd1c4a0 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -156,7 +156,7 @@ def visit_join_expr(self, node: ast.JoinExpr) -> JoinExprResponse: join_strings.append(node.join_type) if isinstance(node.symbol, ast.TableAliasSymbol): - table_symbol = node.symbol.table + table_symbol = node.symbol.table_symbol if table_symbol is None: raise ValueError(f"Table alias {node.symbol.name} does not resolve!") if not isinstance(table_symbol, ast.TableSymbol): @@ -361,7 +361,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): return self.visit( ast.AsteriskSymbol( table=ast.TableAliasSymbol( - table=ast.TableSymbol(table=resolved_field), name=symbol.table.name + table_symbol=ast.TableSymbol(table=resolved_field), name=symbol.table.name ) ) ) @@ -401,7 +401,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): # check for a materialised column table = field_symbol.table while isinstance(table, ast.TableAliasSymbol): - table = table.table + table = table.table_symbol if isinstance(table, ast.TableSymbol): table_name = table.table.clickhouse_table() if field is None: diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index e28cda919ffad..37a684d1890d9 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -88,7 +88,7 @@ def visit_join_expr(self, node): if table_alias == table_name: node.symbol = node.table.symbol else: - node.symbol = ast.TableAliasSymbol(name=table_alias, table=node.table.symbol) + node.symbol = ast.TableAliasSymbol(name=table_alias, table_symbol=node.table.symbol) scope.tables[table_alias] = node.symbol else: raise ResolverException(f'Unknown table "{table_name}".') diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 41884fb62230d..f68328e3382bf 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -47,7 +47,7 @@ def test_resolve_events_table_alias(self): resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(name="e", table=events_table_symbol) + events_table_alias_symbol = ast.TableAliasSymbol(name="e", table_symbol=events_table_symbol) event_field_symbol = ast.FieldSymbol(name="event", table=events_table_alias_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) select_query_symbol = ast.SelectQuerySymbol( @@ -85,7 +85,7 @@ def test_resolve_events_table_column_alias(self): resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(name="e", table=events_table_symbol) + events_table_alias_symbol = ast.TableAliasSymbol(name="e", table_symbol=events_table_symbol) event_field_symbol = ast.FieldSymbol(name="event", table=events_table_alias_symbol) timestamp_field_symbol = ast.FieldSymbol(name="timestamp", table=events_table_alias_symbol) @@ -333,7 +333,7 @@ def test_resolve_lazy_events_pdi_table_aliased(self): expr = parse_select("select event, e.pdi.person_id from events e") resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + events_table_alias_symbol = ast.TableAliasSymbol(table_symbol=events_table_symbol, name="e") expected = ast.SelectQuery( select=[ ast.Field( @@ -435,7 +435,7 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): expr = parse_select("select event, e.pdi.person.id from events e") resolve_symbols(expr) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + events_table_alias_symbol = ast.TableAliasSymbol(table_symbol=events_table_symbol, name="e") expected = ast.SelectQuery( select=[ ast.Field( diff --git a/posthog/hogql/transforms.py b/posthog/hogql/transforms.py index b3d8c91ee5cf0..a2c879f00ca0b 100644 --- a/posthog/hogql/transforms.py +++ b/posthog/hogql/transforms.py @@ -19,7 +19,7 @@ def visit_select_query(self, node: ast.SelectQuery): if isinstance(asterisk.table, ast.TableSymbol) or isinstance(asterisk.table, ast.TableAliasSymbol): table = asterisk.table while isinstance(table, ast.TableAliasSymbol): - table = table.table + table = table.table_symbol if isinstance(table, ast.TableSymbol): database_fields = table.table.get_asterisk() for key in database_fields.keys(): diff --git a/posthog/hogql/transforms/asterisk.py b/posthog/hogql/transforms/asterisk.py index 54eebd4086e0c..40aada63fd37f 100644 --- a/posthog/hogql/transforms/asterisk.py +++ b/posthog/hogql/transforms/asterisk.py @@ -23,7 +23,7 @@ def visit_select_query(self, node: ast.SelectQuery): ): table = asterisk.table while isinstance(table, ast.TableAliasSymbol): - table = table.table + table = table.table_symbol if isinstance(table, ast.TableSymbol): database_fields = table.table.get_asterisk() for key in database_fields.keys(): diff --git a/posthog/hogql/transforms/test/test_asterisk.py b/posthog/hogql/transforms/test/test_asterisk.py index 1504c338d4089..b189e4ef754e5 100644 --- a/posthog/hogql/transforms/test/test_asterisk.py +++ b/posthog/hogql/transforms/test/test_asterisk.py @@ -32,7 +32,7 @@ def test_asterisk_expander_table_alias(self): resolve_symbols(node) expand_asterisks(node) events_table_symbol = ast.TableSymbol(table=database.events) - events_table_alias_symbol = ast.TableAliasSymbol(table=events_table_symbol, name="e") + events_table_alias_symbol = ast.TableAliasSymbol(table_symbol=events_table_symbol, name="e") self.assertEqual( node.select, [ diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index b72a5529325f1..fe5b982429467 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -104,7 +104,7 @@ def visit_lazy_table_symbol(self, node: ast.LazyTableSymbol): self.visit(node.table) def visit_table_alias_symbol(self, node: ast.TableAliasSymbol): - self.visit(node.table) + self.visit(node.table_symbol) def visit_select_query_alias_symbol(self, node: ast.SelectQueryAliasSymbol): self.visit(node.symbol) From e7599f3bc25503a80fc82879d9e0769a300945fa Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 22:05:36 +0100 Subject: [PATCH 127/142] easier this way --- posthog/hogql/ast.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 9ae48a3fc6305..909ed0889da98 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -72,10 +72,7 @@ def get_child(self, name: str) -> Symbol: if name == "*": return AsteriskSymbol(table=self) if self.has_child(name): - table_symbol: Union[TableSymbol, TableAliasSymbol] = self - while isinstance(table_symbol, TableAliasSymbol): - table_symbol = table_symbol.table_symbol - field = table_symbol.table.get_field(name) + field = self.table_symbol.table.get_field(name) return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f"Field not found: {name}") From 5024a4216b8f840af58559be9098b3e4d66f932e Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 22:46:45 +0100 Subject: [PATCH 128/142] more comments and renames --- posthog/hogql/ast.py | 20 +++---- posthog/hogql/test/test_resolver.py | 28 +++++----- posthog/hogql/transforms/asterisk.py | 2 +- posthog/hogql/transforms/lazy_tables.py | 70 +++++++++++++++---------- 4 files changed, 67 insertions(+), 53 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 909ed0889da98..2d7839a952444 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -80,16 +80,16 @@ def get_child(self, name: str) -> Symbol: class LazyTableSymbol(Symbol): table: Union[TableSymbol, TableAliasSymbol, "LazyTableSymbol"] field: str - joined_table: LazyTable + lazy_table: LazyTable def has_child(self, name: str) -> bool: - return self.joined_table.table.has_field(name) + return self.lazy_table.table.has_field(name) def get_child(self, name: str) -> Symbol: if name == "*": return AsteriskSymbol(table=self) if self.has_child(name): - field = self.joined_table.table.get_field(name) + field = self.lazy_table.table.get_field(name) return database_field_to_symbol(field, name, table_symbol=self) raise ValueError(f"Field not found: {name}") @@ -98,7 +98,7 @@ def database_field_to_symbol( field: BaseModel, name: str, table_symbol: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol] ) -> Symbol: if isinstance(field, LazyTable): - return LazyTableSymbol(table=table_symbol, field=name, joined_table=field) + return LazyTableSymbol(table=table_symbol, field=name, lazy_table=field) if isinstance(field, FieldTraverser): return FieldTraverserSymbol(chain=field.chain, symbol=table_symbol) return FieldSymbol(name=name, table=table_symbol) @@ -116,12 +116,14 @@ class SelectQuerySymbol(Symbol): # all from and join subqueries without aliases anonymous_tables: List["SelectQuerySymbol"] = PydanticField(default_factory=list) - def key_for_table( + def get_alias_for_table_symbol( self, - table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"], + table_symbol: Union[ + TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol" + ], ) -> Optional[str]: for key, value in self.tables.items(): - if value == table: + if value == table_symbol: return key return None @@ -179,7 +181,7 @@ class FieldSymbol(Symbol): def resolve_database_table(self) -> Optional[Table]: table_symbol = self.table if isinstance(table_symbol, LazyTableSymbol): - return table_symbol.joined_table.table + return table_symbol.lazy_table.table while isinstance(table_symbol, TableAliasSymbol): table_symbol = table_symbol.table_symbol if isinstance(table_symbol, TableSymbol): @@ -199,7 +201,7 @@ def get_child(self, name: str) -> Symbol: if isinstance(database_field, LazyTable): return FieldSymbol( - name=name, table=LazyTableSymbol(table=self.table, field=name, joined_table=database_field) + name=name, table=LazyTableSymbol(table=self.table, field=name, lazy_table=database_field) ) if isinstance(database_field, StringJSONDatabaseField): return PropertySymbol(name=name, parent=self) diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index f68328e3382bf..8dddfe9fb0959 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -250,7 +250,7 @@ def test_resolve_lazy_pdi_person_table(self): symbol=ast.FieldSymbol( name="id", table=ast.LazyTableSymbol( - table=pdi_table_symbol, field="person", joined_table=database.person_distinct_ids.person + table=pdi_table_symbol, field="person", lazy_table=database.person_distinct_ids.person ), ), ), @@ -268,7 +268,7 @@ def test_resolve_lazy_pdi_person_table(self): name="id", table=ast.LazyTableSymbol( table=pdi_table_symbol, - joined_table=database.person_distinct_ids.person, + lazy_table=database.person_distinct_ids.person, field="person", ), ), @@ -297,7 +297,7 @@ def test_resolve_lazy_events_pdi_table(self): symbol=ast.FieldSymbol( name="person_id", table=ast.LazyTableSymbol( - table=events_table_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_symbol, field="pdi", lazy_table=database.events.pdi ), ), ), @@ -315,7 +315,7 @@ def test_resolve_lazy_events_pdi_table(self): name="person_id", table=ast.LazyTableSymbol( table=events_table_symbol, - joined_table=database.events.pdi, + lazy_table=database.events.pdi, field="pdi", ), ), @@ -345,7 +345,7 @@ def test_resolve_lazy_events_pdi_table_aliased(self): symbol=ast.FieldSymbol( name="person_id", table=ast.LazyTableSymbol( - table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_alias_symbol, field="pdi", lazy_table=database.events.pdi ), ), ), @@ -364,7 +364,7 @@ def test_resolve_lazy_events_pdi_table_aliased(self): name="person_id", table=ast.LazyTableSymbol( table=events_table_alias_symbol, - joined_table=database.events.pdi, + lazy_table=database.events.pdi, field="pdi", ), ), @@ -394,10 +394,10 @@ def test_resolve_lazy_events_pdi_person_table(self): name="id", table=ast.LazyTableSymbol( table=ast.LazyTableSymbol( - table=events_table_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_symbol, field="pdi", lazy_table=database.events.pdi ), field="person", - joined_table=database.events.pdi.table.person, + lazy_table=database.events.pdi.table.person, ), ), ), @@ -415,10 +415,10 @@ def test_resolve_lazy_events_pdi_person_table(self): name="id", table=ast.LazyTableSymbol( table=ast.LazyTableSymbol( - table=events_table_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_symbol, field="pdi", lazy_table=database.events.pdi ), field="person", - joined_table=database.events.pdi.table.person, + lazy_table=database.events.pdi.table.person, ), ), }, @@ -448,10 +448,10 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): name="id", table=ast.LazyTableSymbol( table=ast.LazyTableSymbol( - table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_alias_symbol, field="pdi", lazy_table=database.events.pdi ), field="person", - joined_table=database.events.pdi.table.person, + lazy_table=database.events.pdi.table.person, ), ), ), @@ -470,10 +470,10 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): name="id", table=ast.LazyTableSymbol( table=ast.LazyTableSymbol( - table=events_table_alias_symbol, field="pdi", joined_table=database.events.pdi + table=events_table_alias_symbol, field="pdi", lazy_table=database.events.pdi ), field="person", - joined_table=database.events.pdi.table.person, + lazy_table=database.events.pdi.table.person, ), ), }, diff --git a/posthog/hogql/transforms/asterisk.py b/posthog/hogql/transforms/asterisk.py index 40aada63fd37f..a8ccc94cbb27f 100644 --- a/posthog/hogql/transforms/asterisk.py +++ b/posthog/hogql/transforms/asterisk.py @@ -31,7 +31,7 @@ def visit_select_query(self, node: ast.SelectQuery): columns.append(ast.Field(chain=[key], symbol=symbol)) node.symbol.columns[key] = symbol elif isinstance(table, ast.LazyTableSymbol): - database_fields = table.joined_table.table.get_asterisk() + database_fields = table.lazy_table.table.get_asterisk() for key in database_fields.keys(): symbol = ast.FieldSymbol(name=key, table=asterisk.table) columns.append(ast.Field(chain=[key], symbol=symbol)) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 838e36ce7c8c9..a63abb1adbd8a 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -1,7 +1,9 @@ import dataclasses -from typing import Callable, Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Set, Union from posthog.hogql import ast +from posthog.hogql.ast import LazyTableSymbol +from posthog.hogql.database import LazyTable from posthog.hogql.resolver import resolve_symbols from posthog.hogql.visitor import TraversingVisitor @@ -13,6 +15,14 @@ def resolve_lazy_tables(node: ast.Expr, stack: Optional[List[ast.SelectQuery]] = LazyTableResolver(stack=stack).visit(node) +@dataclasses.dataclass +class JoinToAdd: + fields_accessed: Set[str] + lazy_table: LazyTable + from_table: str + to_table: str + + class LazyTableResolver(TraversingVisitor): def __init__(self, stack: Optional[List[ast.SelectQuery]] = None): super().__init__() @@ -22,7 +32,7 @@ def _get_long_table_name( self, select: ast.SelectQuerySymbol, symbol: Union[ast.TableSymbol, ast.LazyTableSymbol, ast.TableAliasSymbol] ) -> str: if isinstance(symbol, ast.TableSymbol): - return select.key_for_table(symbol) + return select.get_alias_for_table_symbol(symbol) elif isinstance(symbol, ast.TableAliasSymbol): return symbol.name elif isinstance(symbol, ast.LazyTableSymbol): @@ -32,6 +42,7 @@ def _get_long_table_name( def visit_field_symbol(self, node: ast.FieldSymbol): if isinstance(node.table, ast.LazyTableSymbol): + # 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") self.stack_of_fields[-1].append(node) @@ -41,58 +52,58 @@ def visit_select_query(self, node: ast.SelectQuery): if not select_symbol: raise ValueError("Select query must have a symbol") - # Collects each `ast.Field` with `ast.LazyTableSymbol` + # Collect each `ast.Field` with `ast.LazyTableSymbol` field_collector: List[ast.FieldSymbol] = [] self.stack_of_fields.append(field_collector) + # Collect all visited fields on lazy tables into field_collector super().visit_select_query(node) - @dataclasses.dataclass - class JoinToAdd: - fields_accessed: Set[str] - join_function: Callable[[str, str, List[str]], ast.JoinExpr] - from_table: str - from_field: str - to_table: str - + # Collect all the joins we need to add to the select query joins_to_add: Dict[str, JoinToAdd] = {} - for field in field_collector: - lazy_table = field.table - # traverse the lazy tables to a real table, then loop over them in reverse order to create the joins - joins_for_field: List = [] - while isinstance(lazy_table, ast.LazyTableSymbol): - joins_for_field.append(lazy_table) - lazy_table = lazy_table.table - for lazy_table in reversed(joins_for_field): - from_table = self._get_long_table_name(select_symbol, lazy_table.table) - to_table = self._get_long_table_name(select_symbol, lazy_table) + table_symbol = field.table + + # Traverse the lazy tables until we reach a real table, collecting them in a list. + # Usually there's just one or two. + table_symbols: List[LazyTableSymbol] = [] + while isinstance(table_symbol, ast.LazyTableSymbol): + table_symbols.append(table_symbol) + table_symbol = table_symbol.table + + # Loop over the collected lazy tables in reverse order to create the joins + for table_symbol in reversed(table_symbols): + from_table = self._get_long_table_name(select_symbol, table_symbol.table) + to_table = self._get_long_table_name(select_symbol, table_symbol) if to_table not in joins_to_add: joins_to_add[to_table] = JoinToAdd( - fields_accessed=set(), - join_function=lazy_table.joined_table.join_function, + fields_accessed=set(), # collect here all fields accessed on this table + lazy_table=table_symbol.lazy_table, from_table=from_table, - from_field=lazy_table.joined_table.from_field, to_table=to_table, ) new_join = joins_to_add[to_table] - if lazy_table == field.table: + if table_symbol == field.table: new_join.fields_accessed.add(field.name) - # Make sure we also add the join "ON" condition fields into the list of fields accessed. - # Without this "events.pdi.person.anything" won't work without ALSO selecting "events.pdi.person_id" explicitly + # Make sure we also add fields we will use for the join's "ON" condition into the list of fields accessed. + # Without thi "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.add(new_join.from_field) + joins_to_add[new_join.from_table].fields_accessed.add(new_join.lazy_table.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.join_function(scope.from_table, scope.to_table, list(scope.fields_accessed)) + next_join = scope.lazy_table.join_function(scope.from_table, scope.to_table, list(scope.fields_accessed)) resolve_symbols(next_join, select_symbol) select_symbol.tables[to_table] = next_join.symbol + + # Link up the joins properly if last_join is None: node.select_from = next_join last_join = next_join @@ -101,6 +112,7 @@ class JoinToAdd: while last_join.next_join is not None: last_join = last_join.next_join + # Assign all symbols on the fields we collected earlier for field in field_collector: to_table = self._get_long_table_name(select_symbol, field.table) field.table = select_symbol.tables[to_table] From fe582b43506069a5cb5a2979a816505d03423e6d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 22:52:23 +0100 Subject: [PATCH 129/142] keep name in alignment with other similar objects, remove code that's not used --- posthog/hogql/ast.py | 11 ++--------- posthog/hogql/resolver.py | 2 +- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 2d7839a952444..192fc50bda948 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -100,7 +100,7 @@ def database_field_to_symbol( if isinstance(field, LazyTable): return LazyTableSymbol(table=table_symbol, field=name, lazy_table=field) if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, symbol=table_symbol) + return FieldTraverserSymbol(chain=field.chain, table=table_symbol) return FieldSymbol(name=name, table=table_symbol) @@ -171,7 +171,7 @@ class AsteriskSymbol(Symbol): class FieldTraverserSymbol(Symbol): chain: List[str] - symbol: Symbol + table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldSymbol(Symbol): @@ -198,15 +198,8 @@ def get_child(self, name: str) -> Symbol: database_field = self.resolve_database_field() if database_field is None: raise ValueError(f'Can not access property "{name}" on field "{self.name}".') - - if isinstance(database_field, LazyTable): - return FieldSymbol( - name=name, table=LazyTableSymbol(table=self.table, field=name, lazy_table=database_field) - ) if isinstance(database_field, StringJSONDatabaseField): return PropertySymbol(name=name, parent=self) - if isinstance(database_field, FieldTraverser): - return FieldTraverserSymbol(chain=database_field.chain, symbol=self) raise ValueError( f'Can not access property "{name}" on field "{self.name}" of type: {type(database_field).__name__}' ) diff --git a/posthog/hogql/resolver.py b/posthog/hogql/resolver.py index 37a684d1890d9..5504805aada30 100644 --- a/posthog/hogql/resolver.py +++ b/posthog/hogql/resolver.py @@ -181,7 +181,7 @@ def visit_field(self, node): while True: if isinstance(loop_symbol, FieldTraverserSymbol): chain_to_parse = loop_symbol.chain + chain_to_parse - loop_symbol = loop_symbol.symbol + loop_symbol = loop_symbol.table continue if len(chain_to_parse) == 0: break From 9d1cb000d0c059f28753f4779b853873bab9d131 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Wed, 22 Feb 2023 23:55:21 +0100 Subject: [PATCH 130/142] reduce duplication --- posthog/hogql/ast.py | 114 ++++++++++-------------- posthog/hogql/transforms/lazy_tables.py | 6 +- 2 files changed, 47 insertions(+), 73 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 192fc50bda948..6039ca8c4b62d 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -35,73 +35,60 @@ def has_child(self, name: str) -> bool: return self.get_child(name) is not None -class FieldAliasSymbol(Symbol): - name: str - symbol: Symbol - - def get_child(self, name: str) -> Symbol: - return self.symbol.get_child(name) +class TableLikeSymbol(Symbol): + def resolve_database_table(self) -> Table: + raise NotImplementedError("TableLikeSymbol.resolve_database_table not overridden") def has_child(self, name: str) -> bool: - return self.symbol.has_child(name) - - -class TableSymbol(Symbol): - table: Table - - def has_child(self, name: str) -> bool: - return self.table.has_field(name) + return self.resolve_database_table().has_field(name) def get_child(self, name: str) -> Symbol: if name == "*": return AsteriskSymbol(table=self) if self.has_child(name): - field = self.table.get_field(name) - return database_field_to_symbol(field, name, table_symbol=self) - raise ValueError(f'Field "{name}" not found on table {type(self.table).__name__}') + field = self.resolve_database_table().get_field(name) + if isinstance(field, LazyTable): + return LazyTableSymbol(table=self, field=name, lazy_table=field) + if isinstance(field, FieldTraverser): + return FieldTraverserSymbol(chain=field.chain, table=self) + return FieldSymbol(name=name, table=self) + raise ValueError(f"Field not found: {name}") + + +class TableSymbol(TableLikeSymbol): + table: Table + + def resolve_database_table(self) -> Table: + return self.table -class TableAliasSymbol(Symbol): +class TableAliasSymbol(TableLikeSymbol): name: str table_symbol: TableSymbol - def has_child(self, name: str) -> bool: - return self.table_symbol.has_child(name) + def resolve_database_table(self) -> Table: + return self.table_symbol.table - def get_child(self, name: str) -> Symbol: - if name == "*": - return AsteriskSymbol(table=self) - if self.has_child(name): - field = self.table_symbol.table.get_field(name) - return database_field_to_symbol(field, name, table_symbol=self) - raise ValueError(f"Field not found: {name}") - -class LazyTableSymbol(Symbol): - table: Union[TableSymbol, TableAliasSymbol, "LazyTableSymbol"] +class LazyTableSymbol(TableLikeSymbol): + table: TableLikeSymbol field: str lazy_table: LazyTable - def has_child(self, name: str) -> bool: - return self.lazy_table.table.has_field(name) + def resolve_database_table(self) -> Table: + return self.lazy_table.table - def get_child(self, name: str) -> Symbol: - if name == "*": - return AsteriskSymbol(table=self) - if self.has_child(name): - field = self.lazy_table.table.get_field(name) - return database_field_to_symbol(field, name, table_symbol=self) - raise ValueError(f"Field not found: {name}") +class FieldAliasSymbol(Symbol): + name: str + + symbol: Symbol + + def get_child(self, name: str) -> Symbol: + return self.symbol.get_child(name) -def database_field_to_symbol( - field: BaseModel, name: str, table_symbol: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol] -) -> Symbol: - if isinstance(field, LazyTable): - return LazyTableSymbol(table=table_symbol, field=name, lazy_table=field) - if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, table=table_symbol) - return FieldSymbol(name=name, table=table_symbol) + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) class SelectQuerySymbol(Symbol): @@ -110,17 +97,15 @@ class SelectQuerySymbol(Symbol): # all symbols a select query exports columns: Dict[str, Symbol] = PydanticField(default_factory=dict) # all from and join, tables and subqueries with aliases - tables: Dict[ - str, Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"] - ] = PydanticField(default_factory=dict) + tables: Dict[str, Union[TableLikeSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"]] = PydanticField( + default_factory=dict + ) # all from and join subqueries without aliases anonymous_tables: List["SelectQuerySymbol"] = PydanticField(default_factory=list) def get_alias_for_table_symbol( self, - table_symbol: Union[ - TableSymbol, TableAliasSymbol, LazyTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol" - ], + table_symbol: Union[TableLikeSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"], ) -> Optional[str]: for key, value in self.tables.items(): if value == table_symbol: @@ -166,32 +151,23 @@ class ConstantSymbol(Symbol): class AsteriskSymbol(Symbol): - table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldTraverserSymbol(Symbol): chain: List[str] - table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldSymbol(Symbol): name: str - table: Union[TableSymbol, TableAliasSymbol, LazyTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] - - def resolve_database_table(self) -> Optional[Table]: - table_symbol = self.table - if isinstance(table_symbol, LazyTableSymbol): - return table_symbol.lazy_table.table - while isinstance(table_symbol, TableAliasSymbol): - table_symbol = table_symbol.table_symbol - if isinstance(table_symbol, TableSymbol): - return table_symbol.table - return None + table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] def resolve_database_field(self) -> Optional[DatabaseField]: - table = self.resolve_database_table() - if table is not None: - return table.get_field(self.name) + if isinstance(self.table, TableLikeSymbol): + table = self.table.resolve_database_table() + if table is not None: + return table.get_field(self.name) return None def get_child(self, name: str) -> Symbol: diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index a63abb1adbd8a..08d64166c35f5 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Dict, List, Optional, Set, Union +from typing import Dict, List, Optional, Set from posthog.hogql import ast from posthog.hogql.ast import LazyTableSymbol @@ -28,9 +28,7 @@ def __init__(self, stack: Optional[List[ast.SelectQuery]] = None): super().__init__() self.stack_of_fields: List[List[ast.FieldSymbol]] = [[]] if stack else [] - def _get_long_table_name( - self, select: ast.SelectQuerySymbol, symbol: Union[ast.TableSymbol, ast.LazyTableSymbol, ast.TableAliasSymbol] - ) -> str: + def _get_long_table_name(self, select: ast.SelectQuerySymbol, symbol: ast.TableLikeSymbol) -> str: if isinstance(symbol, ast.TableSymbol): return select.get_alias_for_table_symbol(symbol) elif isinstance(symbol, ast.TableAliasSymbol): From 9a512498ae0aa56c7b3c839df3e75b8d1cf508fc Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 23 Feb 2023 00:01:22 +0100 Subject: [PATCH 131/142] put it back, too much noise to remove --- posthog/hogql/ast.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 6039ca8c4b62d..11767b2ff2759 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -35,6 +35,17 @@ def has_child(self, name: str) -> bool: return self.get_child(name) is not None +class FieldAliasSymbol(Symbol): + name: str + symbol: Symbol + + def get_child(self, name: str) -> Symbol: + return self.symbol.get_child(name) + + def has_child(self, name: str) -> bool: + return self.symbol.has_child(name) + + class TableLikeSymbol(Symbol): def resolve_database_table(self) -> Table: raise NotImplementedError("TableLikeSymbol.resolve_database_table not overridden") @@ -79,18 +90,6 @@ def resolve_database_table(self) -> Table: return self.lazy_table.table -class FieldAliasSymbol(Symbol): - name: str - - symbol: Symbol - - def get_child(self, name: str) -> Symbol: - return self.symbol.get_child(name) - - def has_child(self, name: str) -> bool: - return self.symbol.has_child(name) - - class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope aliases: Dict[str, FieldAliasSymbol] = PydanticField(default_factory=dict) From 70a53385754233ca567215978055fc3b9eeced8d Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 23 Feb 2023 00:51:33 +0100 Subject: [PATCH 132/142] add virtual tables --- posthog/hogql/ast.py | 25 +++++++++++++-- posthog/hogql/database.py | 39 ++++++++++++++++++------ posthog/hogql/printer.py | 12 ++++++-- posthog/hogql/transforms/asterisk.py | 29 +++++------------- posthog/hogql/transforms/lazy_tables.py | 2 ++ posthog/hogql/visitor.py | 3 ++ posthog/models/event/query_event_list.py | 2 -- 7 files changed, 73 insertions(+), 39 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 11767b2ff2759..59f0091920047 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -5,7 +5,14 @@ from pydantic import BaseModel, Extra from pydantic import Field as PydanticField -from posthog.hogql.database import DatabaseField, FieldTraverser, LazyTable, StringJSONDatabaseField, Table +from posthog.hogql.database import ( + DatabaseField, + FieldTraverser, + LazyTable, + StringJSONDatabaseField, + Table, + VirtualTable, +) # NOTE: when you add new AST fields or nodes, add them to the Visitor classes in visitor.py as well! @@ -61,7 +68,9 @@ def get_child(self, name: str) -> Symbol: if isinstance(field, LazyTable): return LazyTableSymbol(table=self, field=name, lazy_table=field) if isinstance(field, FieldTraverser): - return FieldTraverserSymbol(chain=field.chain, table=self) + return FieldTraverserSymbol(table=self, chain=field.chain) + if isinstance(field, VirtualTable): + return VirtualTableSymbol(table=self, field=name, virtual_table=field) return FieldSymbol(name=name, table=self) raise ValueError(f"Field not found: {name}") @@ -90,6 +99,18 @@ def resolve_database_table(self) -> Table: return self.lazy_table.table +class VirtualTableSymbol(TableLikeSymbol): + table: TableLikeSymbol + field: str + virtual_table: VirtualTable + + def resolve_database_table(self) -> Table: + return self.virtual_table + + def has_child(self, name: str) -> bool: + return self.virtual_table.has_field(name) + + class SelectQuerySymbol(Symbol): # all aliases a select query has access to in its scope aliases: Dict[str, FieldAliasSymbol] = PydanticField(default_factory=dict) diff --git a/posthog/hogql/database.py b/posthog/hogql/database.py index 2b2ad4329eccf..d5bf4aba7619c 100644 --- a/posthog/hogql/database.py +++ b/posthog/hogql/database.py @@ -48,17 +48,22 @@ def get_field(self, name: str) -> DatabaseField: def clickhouse_table(self): raise NotImplementedError("Table.clickhouse_table not overridden") + def avoid_asterisk_fields(self) -> List[str]: + return [] + def get_asterisk(self) -> Dict[str, DatabaseField]: asterisk: Dict[str, DatabaseField] = {} + fields_to_avoid = self.avoid_asterisk_fields() + ["team_id"] for key, field in self.__fields__.items(): + if key in fields_to_avoid: + continue database_field = field.default - if key == "team_id": - pass # skip team_id - elif isinstance(database_field, DatabaseField): + if isinstance(database_field, DatabaseField): asterisk[key] = database_field elif ( isinstance(database_field, Table) or isinstance(database_field, LazyTable) + or isinstance(database_field, VirtualTable) or isinstance(database_field, FieldTraverser) ): pass # ignore virtual tables for now @@ -76,6 +81,11 @@ class Config: from_field: str +class VirtualTable(Table): + class Config: + extra = Extra.forbid + + class FieldTraverser(BaseModel): class Config: extra = Extra.forbid @@ -83,6 +93,15 @@ 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" + + class PersonsTable(Table): id: StringDatabaseField = StringDatabaseField(name="id") created_at: DateTimeDatabaseField = DateTimeDatabaseField(name="created_at") @@ -144,13 +163,8 @@ class PersonDistinctIdTable(Table): person: LazyTable = LazyTable(from_field="person_id", table=PersonsTable(), join_function=join_with_persons_table) - # Remove the "is_deleted" and "version" columns from "select *" - def get_asterisk(self) -> Dict[str, DatabaseField]: - asterisk: Dict[str, DatabaseField] = {} - for key, value in super().get_asterisk().items(): - if key != "is_deleted" and key != "version": - asterisk[key] = value - return asterisk + def avoid_asterisk_fields(self): + return ["is_deleted", "version"] def clickhouse_table(self): return "person_distinct_id2" @@ -205,9 +219,14 @@ class EventsTable(Table): elements_chain: StringDatabaseField = StringDatabaseField(name="elements_chain") 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 ) + # person fields on the event itself + poe: EventsPersonSubTable = EventsPersonSubTable() + + # TODO: swap these between pdi and person_on_events as needed person: FieldTraverser = FieldTraverser(chain=["pdi", "person"]) person_id: FieldTraverser = FieldTraverser(chain=["pdi", "person_id"]) diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index a595aecd1c4a0..b3b9ae37ea082 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -349,13 +349,16 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): except ResolverException: symbol_with_name_in_scope = None - if isinstance(symbol.table, ast.TableSymbol) or isinstance(symbol.table, ast.TableAliasSymbol): + if ( + isinstance(symbol.table, ast.TableSymbol) + or isinstance(symbol.table, ast.TableAliasSymbol) + or isinstance(symbol.table, ast.VirtualTableSymbol) + ): resolved_field = symbol.resolve_database_field() if resolved_field is None: raise ValueError(f'Can\'t resolve field "{symbol.name}" on table.') if isinstance(resolved_field, Table): - # :KLUDGE: only works for events.person.* printing now - if isinstance(symbol.table, ast.TableSymbol): + if isinstance(symbol.table, ast.VirtualTableSymbol): return self.visit(ast.AsteriskSymbol(table=ast.TableSymbol(table=resolved_field))) else: return self.visit( @@ -441,6 +444,9 @@ def visit_select_query_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): def visit_field_alias_symbol(self, symbol: ast.SelectQueryAliasSymbol): return self._print_identifier(symbol.name) + def visit_virtual_table_symbol(self, symbol: ast.VirtualTableSymbol): + return self.visit(symbol.table) + def visit_asterisk_symbol(self, symbol: ast.AsteriskSymbol): raise ValueError("Unexpected ast.AsteriskSymbol. Make sure AsteriskExpander has run on the AST.") diff --git a/posthog/hogql/transforms/asterisk.py b/posthog/hogql/transforms/asterisk.py index a8ccc94cbb27f..662458ded01bc 100644 --- a/posthog/hogql/transforms/asterisk.py +++ b/posthog/hogql/transforms/asterisk.py @@ -16,28 +16,13 @@ def visit_select_query(self, node: ast.SelectQuery): for column in node.select: if isinstance(column.symbol, ast.AsteriskSymbol): asterisk = column.symbol - if ( - isinstance(asterisk.table, ast.TableSymbol) - or isinstance(asterisk.table, ast.TableAliasSymbol) - or isinstance(asterisk.table, ast.LazyTableSymbol) - ): - table = asterisk.table - while isinstance(table, ast.TableAliasSymbol): - table = table.table_symbol - if isinstance(table, ast.TableSymbol): - database_fields = table.table.get_asterisk() - for key in database_fields.keys(): - symbol = ast.FieldSymbol(name=key, table=asterisk.table) - columns.append(ast.Field(chain=[key], symbol=symbol)) - node.symbol.columns[key] = symbol - elif isinstance(table, ast.LazyTableSymbol): - database_fields = table.lazy_table.table.get_asterisk() - for key in database_fields.keys(): - symbol = ast.FieldSymbol(name=key, table=asterisk.table) - columns.append(ast.Field(chain=[key], symbol=symbol)) - node.symbol.columns[key] = symbol - else: - raise ValueError("Can't expand asterisk (*) on table") + if isinstance(asterisk.table, ast.TableLikeSymbol): + table = asterisk.table.resolve_database_table() + database_fields = table.get_asterisk() + for key in database_fields.keys(): + symbol = ast.FieldSymbol(name=key, table=asterisk.table) + columns.append(ast.Field(chain=[key], symbol=symbol)) + node.symbol.columns[key] = symbol elif isinstance(asterisk.table, ast.SelectQuerySymbol) or isinstance( asterisk.table, ast.SelectQueryAliasSymbol ): diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 08d64166c35f5..d3344822e2421 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -35,6 +35,8 @@ def _get_long_table_name(self, select: ast.SelectQuerySymbol, symbol: ast.TableL return symbol.name elif isinstance(symbol, ast.LazyTableSymbol): return f"{self._get_long_table_name(select, symbol.table)}__{symbol.field}" + elif isinstance(symbol, ast.VirtualTableSymbol): + return f"{self._get_long_table_name(select, symbol.table)}__{symbol.field}" else: raise ValueError("Should not be reachable") diff --git a/posthog/hogql/visitor.py b/posthog/hogql/visitor.py index fe5b982429467..095b341ee30ab 100644 --- a/posthog/hogql/visitor.py +++ b/posthog/hogql/visitor.py @@ -103,6 +103,9 @@ def visit_field_traverser_symbol(self, node: ast.LazyTableSymbol): def visit_lazy_table_symbol(self, node: ast.LazyTableSymbol): self.visit(node.table) + def visit_virtual_table_symbol(self, node: ast.VirtualTableSymbol): + self.visit(node.table) + def visit_table_alias_symbol(self, node: ast.TableAliasSymbol): self.visit(node.table_symbol) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index e36ce15879617..528bc9e4cf4a8 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -209,8 +209,6 @@ def run_events_query( select = ["*"] for expr in select: - if expr == "*": - expr = f"tuple({', '.join(SELECT_STAR_FROM_EVENTS_FIELDS)})" hogql_context.found_aggregation = False if expr == "*": expr = f'tuple({", ".join(SELECT_STAR_FROM_EVENTS_FIELDS)})' From 417682a1d1444e519243095b48b36d378cc36536 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 23 Feb 2023 01:02:07 +0100 Subject: [PATCH 133/142] test for selecting from "poe" --- posthog/hogql/test/test_query.py | 19 ++++++++++++ posthog/hogql/test/test_resolver.py | 45 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/posthog/hogql/test/test_query.py b/posthog/hogql/test/test_query.py index b21a0e224322c..37fcdeecfc39a 100644 --- a/posthog/hogql/test/test_query.py +++ b/posthog/hogql/test/test_query.py @@ -383,3 +383,22 @@ def test_query_joins_events_person_properties_in_aggregration(self): "SELECT s.pdi.person.properties.email, count() FROM events AS s GROUP BY s.pdi.person.properties.email LIMIT 10", ) self.assertEqual(response.results[0][0], "tim@posthog.com") + + def test_select_person_on_events(self): + with freeze_time("2020-01-10"): + self._create_random_events() + response = execute_hogql_query( + "SELECT poe.properties.email, count() FROM events s GROUP BY poe.properties.email LIMIT 10", + self.team, + ) + self.assertEqual( + response.clickhouse, + f"SELECT replaceRegexpAll(JSONExtractRaw(s.person_properties, %(hogql_val_0)s), '^\"|\"$', ''), " + f"count(*) FROM events AS s WHERE equals(s.team_id, {self.team.pk}) GROUP BY " + f"replaceRegexpAll(JSONExtractRaw(s.person_properties, %(hogql_val_1)s), '^\"|\"$', '') LIMIT 10", + ) + self.assertEqual( + response.hogql, + "SELECT poe.properties.email, count() FROM events AS s GROUP BY poe.properties.email LIMIT 10", + ) + self.assertEqual(response.results[0][0], "tim@posthog.com") diff --git a/posthog/hogql/test/test_resolver.py b/posthog/hogql/test/test_resolver.py index 8dddfe9fb0959..af9b9cdddec9d 100644 --- a/posthog/hogql/test/test_resolver.py +++ b/posthog/hogql/test/test_resolver.py @@ -485,3 +485,48 @@ def test_resolve_lazy_events_pdi_person_table_aliased(self): self.assertEqual(expr.where, expected.where) self.assertEqual(expr.symbol, expected.symbol) self.assertEqual(expr, expected) + + def test_resolve_virtual_events_poe(self): + expr = parse_select("select event, poe.id from events") + resolve_symbols(expr) + events_table_symbol = ast.TableSymbol(table=database.events) + expected = ast.SelectQuery( + select=[ + ast.Field( + chain=["event"], + symbol=ast.FieldSymbol(name="event", table=events_table_symbol), + ), + ast.Field( + chain=["poe", "id"], + symbol=ast.FieldSymbol( + name="id", + table=ast.VirtualTableSymbol( + table=events_table_symbol, field="poe", virtual_table=database.events.poe + ), + ), + ), + ], + select_from=ast.JoinExpr( + table=ast.Field(chain=["events"], symbol=events_table_symbol), + symbol=events_table_symbol, + ), + symbol=ast.SelectQuerySymbol( + aliases={}, + anonymous_tables=[], + columns={ + "event": ast.FieldSymbol(name="event", table=events_table_symbol), + "id": ast.FieldSymbol( + name="id", + table=ast.VirtualTableSymbol( + table=events_table_symbol, field="poe", virtual_table=database.events.poe + ), + ), + }, + tables={"events": events_table_symbol}, + ), + ) + self.assertEqual(expr.select, expected.select) + self.assertEqual(expr.select_from, expected.select_from) + self.assertEqual(expr.where, expected.where) + self.assertEqual(expr.symbol, expected.symbol) + self.assertEqual(expr, expected) From 3e5b96e70e4ce87c7d8968d84e2ad3e1245e0555 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Feb 2023 00:39:45 +0000 Subject: [PATCH 134/142] Update 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 267efbdaa04bc..3abadcfd03436 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -684,8 +684,8 @@ HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event IN ['$autocapture', 'user signed up', '$autocapture'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-15 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-22 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-16 00:00:00', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-23 23:59:59', 'UTC') AND (step_0 = 1 OR step_1 = 1) )) WHERE step_0 = 1 )) From 8c58dcdfef2fa3888e8f910f8eaba739b75bf391 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Thu, 23 Feb 2023 09:27:51 +0100 Subject: [PATCH 135/142] Like -> Base --- posthog/hogql/ast.py | 28 ++++++++++++------------- posthog/hogql/transforms/asterisk.py | 2 +- posthog/hogql/transforms/lazy_tables.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/posthog/hogql/ast.py b/posthog/hogql/ast.py index 59f0091920047..55d13dccf5707 100644 --- a/posthog/hogql/ast.py +++ b/posthog/hogql/ast.py @@ -53,9 +53,9 @@ def has_child(self, name: str) -> bool: return self.symbol.has_child(name) -class TableLikeSymbol(Symbol): +class BaseTableSymbol(Symbol): def resolve_database_table(self) -> Table: - raise NotImplementedError("TableLikeSymbol.resolve_database_table not overridden") + raise NotImplementedError("BaseTableSymbol.resolve_database_table not overridden") def has_child(self, name: str) -> bool: return self.resolve_database_table().has_field(name) @@ -75,14 +75,14 @@ def get_child(self, name: str) -> Symbol: raise ValueError(f"Field not found: {name}") -class TableSymbol(TableLikeSymbol): +class TableSymbol(BaseTableSymbol): table: Table def resolve_database_table(self) -> Table: return self.table -class TableAliasSymbol(TableLikeSymbol): +class TableAliasSymbol(BaseTableSymbol): name: str table_symbol: TableSymbol @@ -90,8 +90,8 @@ def resolve_database_table(self) -> Table: return self.table_symbol.table -class LazyTableSymbol(TableLikeSymbol): - table: TableLikeSymbol +class LazyTableSymbol(BaseTableSymbol): + table: BaseTableSymbol field: str lazy_table: LazyTable @@ -99,8 +99,8 @@ def resolve_database_table(self) -> Table: return self.lazy_table.table -class VirtualTableSymbol(TableLikeSymbol): - table: TableLikeSymbol +class VirtualTableSymbol(BaseTableSymbol): + table: BaseTableSymbol field: str virtual_table: VirtualTable @@ -117,7 +117,7 @@ class SelectQuerySymbol(Symbol): # all symbols a select query exports columns: Dict[str, Symbol] = PydanticField(default_factory=dict) # all from and join, tables and subqueries with aliases - tables: Dict[str, Union[TableLikeSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"]] = PydanticField( + tables: Dict[str, Union[BaseTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"]] = PydanticField( default_factory=dict ) # all from and join subqueries without aliases @@ -125,7 +125,7 @@ class SelectQuerySymbol(Symbol): def get_alias_for_table_symbol( self, - table_symbol: Union[TableLikeSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"], + table_symbol: Union[BaseTableSymbol, "SelectQuerySymbol", "SelectQueryAliasSymbol"], ) -> Optional[str]: for key, value in self.tables.items(): if value == table_symbol: @@ -171,20 +171,20 @@ class ConstantSymbol(Symbol): class AsteriskSymbol(Symbol): - table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[BaseTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldTraverserSymbol(Symbol): chain: List[str] - table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[BaseTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] class FieldSymbol(Symbol): name: str - table: Union[TableLikeSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] + table: Union[BaseTableSymbol, SelectQuerySymbol, SelectQueryAliasSymbol] def resolve_database_field(self) -> Optional[DatabaseField]: - if isinstance(self.table, TableLikeSymbol): + if isinstance(self.table, BaseTableSymbol): table = self.table.resolve_database_table() if table is not None: return table.get_field(self.name) diff --git a/posthog/hogql/transforms/asterisk.py b/posthog/hogql/transforms/asterisk.py index 662458ded01bc..b860f22195392 100644 --- a/posthog/hogql/transforms/asterisk.py +++ b/posthog/hogql/transforms/asterisk.py @@ -16,7 +16,7 @@ def visit_select_query(self, node: ast.SelectQuery): for column in node.select: if isinstance(column.symbol, ast.AsteriskSymbol): asterisk = column.symbol - if isinstance(asterisk.table, ast.TableLikeSymbol): + if isinstance(asterisk.table, ast.BaseTableSymbol): table = asterisk.table.resolve_database_table() database_fields = table.get_asterisk() for key in database_fields.keys(): diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index d3344822e2421..4ddb9ac149548 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -28,7 +28,7 @@ def __init__(self, stack: Optional[List[ast.SelectQuery]] = None): super().__init__() self.stack_of_fields: List[List[ast.FieldSymbol]] = [[]] if stack else [] - def _get_long_table_name(self, select: ast.SelectQuerySymbol, symbol: ast.TableLikeSymbol) -> str: + def _get_long_table_name(self, select: ast.SelectQuerySymbol, symbol: ast.BaseTableSymbol) -> str: if isinstance(symbol, ast.TableSymbol): return select.get_alias_for_table_symbol(symbol) elif isinstance(symbol, ast.TableAliasSymbol): From f288e689750d3d11d062ab1b7b1431941930b0f5 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Fri, 24 Feb 2023 17:37:46 +0100 Subject: [PATCH 136/142] small fixes --- posthog/hogql/transforms/lazy_tables.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/posthog/hogql/transforms/lazy_tables.py b/posthog/hogql/transforms/lazy_tables.py index 4ddb9ac149548..6ed2910c94849 100644 --- a/posthog/hogql/transforms/lazy_tables.py +++ b/posthog/hogql/transforms/lazy_tables.py @@ -33,6 +33,8 @@ def _get_long_table_name(self, select: ast.SelectQuerySymbol, symbol: ast.BaseTa return select.get_alias_for_table_symbol(symbol) elif isinstance(symbol, ast.TableAliasSymbol): return symbol.name + elif isinstance(symbol, ast.SelectQueryAliasSymbol): + return symbol.name elif isinstance(symbol, ast.LazyTableSymbol): return f"{self._get_long_table_name(select, symbol.table)}__{symbol.field}" elif isinstance(symbol, ast.VirtualTableSymbol): @@ -99,7 +101,9 @@ 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, list(scope.fields_accessed)) + next_join = scope.lazy_table.join_function( + scope.from_table, scope.to_table, sorted(list(scope.fields_accessed)) + ) resolve_symbols(next_join, select_symbol) select_symbol.tables[to_table] = next_join.symbol From e76a85b9ab70d0f603bf36afa385cf2d2e981bc5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Feb 2023 16:45:22 +0000 Subject: [PATCH 137/142] Update 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 3abadcfd03436..287ee54a8408e 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -684,8 +684,8 @@ HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event IN ['$autocapture', 'user signed up', '$autocapture'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-16 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-23 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-17 00:00:00', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-24 23:59:59', 'UTC') AND (step_0 = 1 OR step_1 = 1) )) WHERE step_0 = 1 )) From 63fadcf6b2e8b1c2736f624eab78b108bbafb127 Mon Sep 17 00:00:00 2001 From: Marius Andra Date: Mon, 27 Feb 2023 09:26:29 +0100 Subject: [PATCH 138/142] rename legacy bool to within_non_hogql_query --- posthog/api/action.py | 2 +- posthog/hogql/context.py | 4 ++-- posthog/hogql/printer.py | 4 ++-- posthog/hogql/test/test_printer.py | 2 +- posthog/models/cohort/util.py | 2 +- posthog/models/event/query_event_list.py | 4 ++-- posthog/models/filters/mixins/hogql.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/posthog/api/action.py b/posthog/api/action.py index 10b7243efcee2..cd497b0e4f7af 100644 --- a/posthog/api/action.py +++ b/posthog/api/action.py @@ -236,7 +236,7 @@ def people(self, request: request.Request, *args: Any, **kwargs: Any) -> Respons def count(self, request: request.Request, **kwargs) -> Response: action = self.get_object() # NOTE: never accepts cohort parameters so no need for explicit person_id_joined_alias - hogql_context = HogQLContext(legacy_person_property_handling=True) + hogql_context = HogQLContext(within_non_hogql_query=True) query, params = format_action_filter(team_id=action.team_id, action=action, hogql_context=hogql_context) if query == "": return Response({"count": 0}) diff --git a/posthog/hogql/context.py b/posthog/hogql/context.py index 6ce3ce85f34f3..7d8045f227524 100644 --- a/posthog/hogql/context.py +++ b/posthog/hogql/context.py @@ -17,8 +17,8 @@ class HogQLContext: # If set, will save string constants to this dict. Inlines strings into the query if None. values: Dict = field(default_factory=dict) # Are we small part of a non-HogQL query? If so, use custom syntax for accessed person properties. - legacy_person_property_handling: bool = False - # Do we need to join the persons table or not. Has effect if legacy_person_property_handling = True + within_non_hogql_query: bool = False + # Do we need to join the persons table or not. Has effect if within_non_hogql_query = True using_person_on_events: bool = True # If set, allows printing full SELECT queries in ClickHouse select_team_id: Optional[int] = None diff --git a/posthog/hogql/printer.py b/posthog/hogql/printer.py index b3b9ae37ea082..32a42378eeaa1 100644 --- a/posthog/hogql/printer.py +++ b/posthog/hogql/printer.py @@ -383,7 +383,7 @@ def visit_field_symbol(self, symbol: ast.FieldSymbol): field_sql = f"{self.visit(symbol.table)}.{field_sql}" # :KLUDGE: Legacy person properties handling. Only used within non-HogQL queries, such as insights. - if self.context.legacy_person_property_handling and field_sql == "events__pdi__person.properties": + if self.context.within_non_hogql_query and field_sql == "events__pdi__person.properties": if self.context.using_person_on_events: field_sql = "person_properties" else: @@ -418,7 +418,7 @@ def visit_property_symbol(self, symbol: ast.PropertySymbol): field_sql = self.visit(field_symbol) property_sql = trim_quotes_expr(f"JSONExtractRaw({field_sql}, %({key})s)") elif ( - self.context.legacy_person_property_handling + self.context.within_non_hogql_query and isinstance(table, ast.SelectQueryAliasSymbol) and table.name == "events__pdi__person" ): diff --git a/posthog/hogql/test/test_printer.py b/posthog/hogql/test/test_printer.py index 06e47992e20b3..5a3db4fdac482 100644 --- a/posthog/hogql/test/test_printer.py +++ b/posthog/hogql/test/test_printer.py @@ -66,7 +66,7 @@ def test_fields_and_properties(self): "replaceRegexpAll(JSONExtractRaw(events__pdi__person.properties, %(hogql_val_0)s), '^\"|\"$', '')", ) - context = HogQLContext(legacy_person_property_handling=True, using_person_on_events=False) + context = HogQLContext(within_non_hogql_query=True, using_person_on_events=False) self.assertEqual( self._expr("person.properties.bla", context), "replaceRegexpAll(JSONExtractRaw(person_props, %(hogql_val_0)s), '^\"|\"$', '')", diff --git a/posthog/models/cohort/util.py b/posthog/models/cohort/util.py index 2accb1452a9d6..9b76bdcb70ed7 100644 --- a/posthog/models/cohort/util.py +++ b/posthog/models/cohort/util.py @@ -230,7 +230,7 @@ def insert_static_cohort(person_uuids: List[Optional[uuid.UUID]], cohort_id: int def recalculate_cohortpeople(cohort: Cohort, pending_version: int) -> Optional[int]: - hogql_context = HogQLContext(legacy_person_property_handling=True) + hogql_context = HogQLContext(within_non_hogql_query=True) cohort_query, cohort_params = format_person_query(cohort, 0, hogql_context) before_count = get_cohort_size(cohort.pk, cohort.team_id) diff --git a/posthog/models/event/query_event_list.py b/posthog/models/event/query_event_list.py index 528bc9e4cf4a8..c84358665f6a3 100644 --- a/posthog/models/event/query_event_list.py +++ b/posthog/models/event/query_event_list.py @@ -76,7 +76,7 @@ def query_events_list( ) -> List: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext(legacy_person_property_handling=True) + hogql_context = HogQLContext(within_non_hogql_query=True) limit += 1 limit_sql = "LIMIT %(limit)s" @@ -145,7 +145,7 @@ def run_events_query( ) -> EventsQueryResponse: # Note: This code is inefficient and problematic, see https://github.com/PostHog/posthog/issues/13485 for details. # To isolate its impact from rest of the queries its queries are run on different nodes as part of "offline" workloads. - hogql_context = HogQLContext(legacy_person_property_handling=True) + hogql_context = HogQLContext(within_non_hogql_query=True) # adding +1 to the limit to check if there's a "next page" after the requested results limit = min(QUERY_MAXIMUM_LIMIT, QUERY_DEFAULT_LIMIT if query.limit is None else query.limit) + 1 diff --git a/posthog/models/filters/mixins/hogql.py b/posthog/models/filters/mixins/hogql.py index cac020aa15cb2..49513ac9fc9e9 100644 --- a/posthog/models/filters/mixins/hogql.py +++ b/posthog/models/filters/mixins/hogql.py @@ -9,7 +9,7 @@ class HogQLParamMixin: @cached_property def hogql_context(self) -> HogQLContext: - context = self.kwargs.get("hogql_context", HogQLContext(legacy_person_property_handling=True)) + context = self.kwargs.get("hogql_context", HogQLContext(within_non_hogql_query=True)) if self.kwargs.get("team"): context.using_person_on_events = self.kwargs["team"].person_on_events_querying_enabled return context From ebe122e106a8d42d57b85700b722ae0fe6d44dee Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 08:35:19 +0000 Subject: [PATCH 139/142] Update UI snapshots for `chromium` (1) --- .../scenes-app-saved-insights--list-view.png | Bin 61375 -> 62059 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/scenes-app-saved-insights--list-view.png b/frontend/__snapshots__/scenes-app-saved-insights--list-view.png index 8e7f4ecd0bfb95d71d778205395425894403167b..a9eb07622812c25b17ef3ca656640afdfeefe63c 100644 GIT binary patch delta 43749 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 BpOgRq From 4591cff1cd57b0cf2f3ec02231110a74b4b1a57a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 08:42:53 +0000 Subject: [PATCH 140/142] 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 287ee54a8408e..5757182eba380 100644 --- a/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr +++ b/posthog/queries/funnels/test/__snapshots__/test_funnel.ambr @@ -684,8 +684,8 @@ HAVING argMax(is_deleted, version) = 0) AS pdi ON e.distinct_id = pdi.distinct_id WHERE team_id = 2 AND event IN ['$autocapture', 'user signed up', '$autocapture'] - AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-17 00:00:00', 'UTC') - AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-24 23:59:59', 'UTC') + AND toTimeZone(timestamp, 'UTC') >= toDateTime('2023-02-20 00:00:00', 'UTC') + AND toTimeZone(timestamp, 'UTC') <= toDateTime('2023-02-27 23:59:59', 'UTC') AND (step_0 = 1 OR step_1 = 1) )) WHERE step_0 = 1 )) From 0b92e1e7e8138853f973f021b6f769dec0c12b4b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Feb 2023 08:56:23 +0000 Subject: [PATCH 141/142] Update UI snapshots for `chromium` (1) --- .../exporter-exporter--dashboard.png | Bin 160689 -> 149062 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/exporter-exporter--dashboard.png b/frontend/__snapshots__/exporter-exporter--dashboard.png index 4a317a6bd1949c8632bfacdbec12efa3c3ca597d..ed99d631deffb3831ed05f647e0403441b10408b 100644 GIT binary patch literal 149062 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

_LRYo2zgP$F(m3VUuV3q&=kXvxLnZ`J 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 nTuV Date: Mon, 27 Feb 2023 15:01:27 +0000 Subject: [PATCH 142/142] Update UI snapshots for `chromium` (1) --- .../exporter-exporter--dashboard.png | Bin 149062 -> 160689 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/frontend/__snapshots__/exporter-exporter--dashboard.png b/frontend/__snapshots__/exporter-exporter--dashboard.png index ed99d631deffb3831ed05f647e0403441b10408b..4a317a6bd1949c8632bfacdbec12efa3c3ca597d 100644 GIT binary patch literal 160689 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

_LRYo2zgP$F(m3VUuV3q&=kXvxLnZ`J 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