Skip to content

Commit

Permalink
feat(mql): Remove all metrics to SnQL translation code (#165)
Browse files Browse the repository at this point in the history
Now that we can fully use MQL in Snuba for all supported queries, we don't need
the SnQL translation at all.
  • Loading branch information
evanh authored Jan 25, 2024
1 parent ef42841 commit ee327ea
Show file tree
Hide file tree
Showing 14 changed files with 113 additions and 1,093 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog and versioning
==========================

2.0.24
------

- Drop support for translating MetricsQuery to SnQL


2.0.23
------
Expand Down
25 changes: 7 additions & 18 deletions snuba_sdk/metrics_query.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from __future__ import annotations

import json
from dataclasses import dataclass, replace
from datetime import datetime
from typing import Any

from snuba_sdk.expressions import Limit, Offset
from snuba_sdk.formula import Formula
from snuba_sdk.metrics_query_visitors import SnQLPrinter, Validator
from snuba_sdk.metrics_query_visitors import Validator
from snuba_sdk.mql_visitor import MQLPrinter
from snuba_sdk.query import BaseQuery
from snuba_sdk.query_visitors import InvalidQueryError
Expand Down Expand Up @@ -79,31 +80,19 @@ def validate(self) -> None:
Validator().visit(self)

def __str__(self) -> str:
result = PRETTY_PRINTER.visit(self)
assert isinstance(result, str)
return result

def serialize(self) -> str:
self.validate()
result = SNQL_PRINTER.visit(self)
assert isinstance(result, str)
return result
result = MQL_PRINTER.visit(self)
return json.dumps(result, indent=4)

def print(self) -> str:
self.validate()
result = PRETTY_PRINTER.visit(self)
assert isinstance(result, str)
return result
result = MQL_PRINTER.visit(self)
return json.dumps(result, indent=4)

def serialize_to_mql(self) -> dict[str, str | dict[str, Any]]:
# TODO: when the new MQL snuba endpoint is ready, this method will replace .serialize()
def serialize(self) -> str | dict[str, Any]:
self.validate()
result = MQL_PRINTER.visit(self)
assert isinstance(result, dict)
return result


SNQL_PRINTER = SnQLPrinter()
PRETTY_PRINTER = SnQLPrinter(pretty=True)
MQL_PRINTER = MQLPrinter()
VALIDATOR = Validator()
154 changes: 0 additions & 154 deletions snuba_sdk/metrics_query_visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,9 @@
# Import the module due to sphinx autodoc problems
# https://github.com/agronholm/sphinx-autodoc-typehints#dealing-with-circular-imports
from snuba_sdk import metrics_query as main
from snuba_sdk.column import Column
from snuba_sdk.conditions import Condition, Op
from snuba_sdk.expressions import Limit, Offset
from snuba_sdk.formula import Formula
from snuba_sdk.metrics_visitors import (
FormulaSnQLVisitor,
RollupSnQLPrinter,
ScopeSnQLPrinter,
TimeseriesSnQLPrinter,
)
from snuba_sdk.timeseries import MetricsScope, Rollup, Timeseries
from snuba_sdk.visitors import Translation


class InvalidMetricsQueryError(Exception):
Expand Down Expand Up @@ -84,151 +75,6 @@ def _visit_indexer_mappings(
raise NotImplementedError


class SnQLPrinter(MetricsQueryVisitor[str]):
def __init__(self, pretty: bool = False) -> None:
self.pretty = pretty
self.expression_visitor = Translation()
self.timeseries_visitor = TimeseriesSnQLPrinter(self.expression_visitor)
self.rollup_visitor = RollupSnQLPrinter(self.expression_visitor)
self.scope_visitor = ScopeSnQLPrinter(self.expression_visitor)
self.formula_visitor = FormulaSnQLVisitor()
self.separator = "\n" if self.pretty else " "
self.match_clause = "MATCH ({entity})"
self.select_clause = "SELECT {select_columns}"
self.groupby_clause = "BY {groupby_columns}"
self.where_clause = "WHERE {where_clauses}"
self.orderby_clause = "ORDER BY {orderby_columns}"
self.limit_clause = "LIMIT {limit}"
self.offset_clause = "OFFSET {offset}"

def _combine(
self, query: main.MetricsQuery, returns: Mapping[str, str | Mapping[str, str]]
) -> str:
query_data = returns["query"]
assert isinstance(query_data, dict)

entity = query_data["entity"]

select_columns = []
select_columns.append(query_data["aggregate"])

rollup_data = returns["rollup"]
assert isinstance(rollup_data, dict)

groupby_columns = []
orderby_columns = []
where_clauses = []

# Rollup information is a little complicated
# If an interval is specified, then we need to use the interval
# function to properly group by and order the query.
# If totals is specified, then the order by will come from the
# rollup itself and we don't use interval data.
if rollup_data["interval"]:
groupby_columns.append(rollup_data["interval"])
if rollup_data["orderby"]:
orderby_columns.append(rollup_data["orderby"])
where_clauses.append(rollup_data["filter"])

if query_data["groupby"]:
groupby_columns.append(query_data["groupby"])

if "metric_filter" in query_data:
where_clauses.append(query_data["metric_filter"])
if "filters" in query_data and query_data["filters"]:
where_clauses.append(query_data["filters"])

where_clauses.append(returns["scope"])
where_clauses.append(returns["start"])
where_clauses.append(returns["end"])

groupby_clause = (
self.groupby_clause.format(groupby_columns=", ".join(groupby_columns))
if groupby_columns
else ""
)
orderby_clause = (
self.orderby_clause.format(orderby_columns=", ".join(orderby_columns))
if orderby_columns
else ""
)

limit_clause = ""
if returns["limit"]:
limit_clause = self.limit_clause.format(limit=returns["limit"])

offset_clause = ""
if returns["offset"]:
offset_clause = self.offset_clause.format(offset=returns["offset"])

totals_clause = ""
if rollup_data["with_totals"]:
totals_clause = rollup_data["with_totals"]

clauses = [
self.match_clause.format(entity=entity),
self.select_clause.format(select_columns=", ".join(select_columns)),
groupby_clause,
self.where_clause.format(where_clauses=" AND ".join(where_clauses)),
orderby_clause,
limit_clause,
offset_clause,
totals_clause,
]

return self.separator.join(filter(lambda x: x != "", clauses)).strip()

def _visit_query(self, query: Timeseries | Formula | None) -> Mapping[str, str]:
if query is None:
raise InvalidMetricsQueryError("MetricQuery.query must not be None")
if isinstance(query, Formula):
return self.formula_visitor.visit(query)

return self.timeseries_visitor.visit(query)

def _visit_start(self, start: datetime | None) -> str:
if start is None:
raise InvalidMetricsQueryError("MetricQuery.start must not be None")

condition = Condition(Column("timestamp"), Op.GTE, start)
return self.expression_visitor.visit(condition)

def _visit_end(self, end: datetime | None) -> str:
if end is None:
raise InvalidMetricsQueryError("MetricQuery.end must not be None")

condition = Condition(Column("timestamp"), Op.LT, end)
return self.expression_visitor.visit(condition)

def _visit_rollup(self, rollup: Rollup | None) -> Mapping[str, str]:
if rollup is None:
raise InvalidMetricsQueryError("MetricQuery.rollup must not be None")

return self.rollup_visitor.visit(rollup)

def _visit_scope(self, scope: MetricsScope | None) -> str | Mapping[str, str]:
if scope is None:
raise InvalidMetricsQueryError("MetricQuery.scope must not be None")

return self.scope_visitor.visit(scope)

def _visit_limit(self, limit: Limit | None) -> str:
if limit is not None:
return self.expression_visitor.visit(limit)
return ""

def _visit_offset(self, offset: Offset | None) -> str:
if offset is not None:
return self.expression_visitor.visit(offset)
return ""

def _visit_indexer_mappings(
self, indexer_mappings: dict[str, str | int] | None
) -> str:
# SnQL is a temporary measure and assumes the queries are resolved in Sentry
return ""


class Validator(MetricsQueryVisitor[None]):
def _combine(
self, query: main.MetricsQuery, returns: Mapping[str, None | Mapping[str, None]]
Expand Down
Loading

0 comments on commit ee327ea

Please sign in to comment.