Skip to content

Commit

Permalink
handle mappings for field on field filters
Browse files Browse the repository at this point in the history
  • Loading branch information
pblankley committed Feb 6, 2025
1 parent e2d02c8 commit 24c10d4
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 19 deletions.
36 changes: 24 additions & 12 deletions metrics_layer/core/model/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -549,29 +549,41 @@ def sql_query(sql_to_compare: str, expression_type: str, value, field_datatype:
MetricsLayerFilterExpressionType.GreaterOrEqualThan: lambda f: f >= value,
MetricsLayerFilterExpressionType.GreaterThan: lambda f: f > value,
MetricsLayerFilterExpressionType.Like: lambda f: f.like(value),
MetricsLayerFilterExpressionType.Contains: lambda f: f.like(f"%{value}%"),
MetricsLayerFilterExpressionType.DoesNotContain: lambda f: f.not_like(f"%{value}%"),
MetricsLayerFilterExpressionType.Contains: lambda f: f.like(f"%{value}%")
if isinstance(value, str)
else f.like(value),
MetricsLayerFilterExpressionType.DoesNotContain: lambda f: f.not_like(f"%{value}%")
if isinstance(value, str)
else f.not_like(value),
MetricsLayerFilterExpressionType.ContainsCaseInsensitive: lambda f: Lower(f).like(
Lower(f"%{value}%")
Lower(f"%{value}%") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.DoesNotContainCaseInsensitive: lambda f: Lower(f).not_like(
Lower(f"%{value}%")
Lower(f"%{value}%") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.StartsWith: lambda f: f.like(f"{value}%"),
MetricsLayerFilterExpressionType.EndsWith: lambda f: f.like(f"%{value}"),
MetricsLayerFilterExpressionType.DoesNotStartWith: lambda f: f.not_like(f"{value}%"),
MetricsLayerFilterExpressionType.DoesNotEndWith: lambda f: f.not_like(f"%{value}"),
MetricsLayerFilterExpressionType.StartsWith: lambda f: f.like(f"{value}%")
if isinstance(value, str)
else f.like(value),
MetricsLayerFilterExpressionType.EndsWith: lambda f: f.like(f"%{value}")
if isinstance(value, str)
else f.like(value),
MetricsLayerFilterExpressionType.DoesNotStartWith: lambda f: f.not_like(f"{value}%")
if isinstance(value, str)
else f.not_like(value),
MetricsLayerFilterExpressionType.DoesNotEndWith: lambda f: f.not_like(f"%{value}")
if isinstance(value, str)
else f.not_like(value),
MetricsLayerFilterExpressionType.StartsWithCaseInsensitive: lambda f: Lower(f).like(
Lower(f"{value}%")
Lower(f"{value}%") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.EndsWithCaseInsensitive: lambda f: Lower(f).like(
Lower(f"%{value}")
Lower(f"%{value}") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.DoesNotStartWithCaseInsensitive: lambda f: Lower(f).not_like(
Lower(f"{value}%")
Lower(f"{value}%") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.DoesNotEndWithCaseInsensitive: lambda f: Lower(f).not_like(
Lower(f"%{value}")
Lower(f"%{value}") if isinstance(value, str) else Lower(value)
),
MetricsLayerFilterExpressionType.IsNull: lambda f: f.isnull(),
MetricsLayerFilterExpressionType.IsNotNull: lambda f: f.notnull(),
Expand Down
2 changes: 1 addition & 1 deletion metrics_layer/core/sql/merged_query_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ def __init__(
self.project = project
self.metrics = metrics
self.dimensions = dimensions
self.parse_field_names(where, having, order_by)
self.model = model
self.parse_field_names(where, having, order_by)
self.query_type = None

def get_query(self, semicolon: bool = True):
Expand Down
4 changes: 4 additions & 0 deletions metrics_layer/core/sql/resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ def _replace_dict_or_literal(self, where, to_replace, field):
for w in where:
if "group_by" in w and w["group_by"] == to_replace:
result.append({**w, "group_by": field.id()})
elif "field" in w and w["field"] == to_replace and "value" in w and w["value"] == to_replace:
result.append({**w, "field": field.id(), "value": field.id()})
elif "value" in w and w["value"] == to_replace:
result.append({**w, "value": field.id()})
elif "field" in w and w["field"] == to_replace:
result.append({**w, "field": field.id()})
elif "field" not in w and "conditions" in w:
Expand Down
9 changes: 6 additions & 3 deletions metrics_layer/core/sql/single_query_resolve.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ def __init__(
self.metrics = metrics
self.dimensions = dimensions
self.funnel, self.is_funnel_query = self.parse_funnel(funnel)
self.parse_field_names(where, having, order_by)
self.model = model
self.parse_field_names(where, having, order_by)
self.nesting_depth = kwargs.get("nesting_depth", 0)
self.query_type = kwargs.get("query_type")
if self.query_type is None:
Expand Down Expand Up @@ -226,8 +226,7 @@ def parse_funnel(self, funnel: dict):
def _is_literal(clause):
return isinstance(clause, str) or clause is None

@staticmethod
def parse_identifiers_from_dicts(conditions: list):
def parse_identifiers_from_dicts(self, conditions: list):
flattened_conditions = SingleSQLQueryResolver.flatten_filters(conditions)
try:
field_names = []
Expand All @@ -236,6 +235,10 @@ def parse_identifiers_from_dicts(conditions: list):
field_names.append(cond["group_by"])
else:
field_names.append(cond["field"])
if "value" in cond and isinstance(cond["value"], str):
mapped_field = self.project.get_mapped_field(cond["value"], model=self.model)
if mapped_field:
field_names.append(cond["value"])
return field_names
except KeyError:
for cond in conditions:
Expand Down
30 changes: 28 additions & 2 deletions tests/test_join_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,7 @@ def test_join_query_field_on_field_filter(connection):
"value": "customers.first_order_date",
},
{"field": "orders.new_vs_repeat", "expression": "equal_to", "value": "orders.campaign"},
{"field": "orders.new_vs_repeat", "expression": "contains", "value": "orders.campaign"},
{
"field": "orders.revenue_in_cents",
"expression": "equal_to",
Expand All @@ -1731,7 +1732,32 @@ def test_join_query_field_on_field_filter(connection):
" analytics.orders orders LEFT JOIN analytics.customers customers ON"
" orders.customer_id=customers.customer_id WHERE DATE_TRUNC('DAY',"
" orders.order_date)>DATE_TRUNC('DAY', customers.first_order_date) AND"
" orders.new_vs_repeat=orders.campaign AND orders.revenue * 100=orders.revenue GROUP BY"
" orders.new_vs_repeat ORDER BY orders_number_of_orders DESC NULLS LAST;"
" orders.new_vs_repeat=orders.campaign AND orders.new_vs_repeat LIKE orders.campaign AND"
" orders.revenue * 100=orders.revenue GROUP BY orders.new_vs_repeat ORDER BY orders_number_of_orders"
" DESC NULLS LAST;"
)
assert query == correct


@pytest.mark.query
def test_join_query_field_on_field_filter_with_mapping(connection):
query = connection.get_sql_query(
metrics=["number_of_orders"],
dimensions=["new_vs_repeat"],
where=[
{
"field": "customers.first_order_date",
"expression": "greater_than",
"value": "date",
}
],
)

correct = (
"SELECT orders.new_vs_repeat as orders_new_vs_repeat,COUNT(orders.id) as orders_number_of_orders FROM"
" analytics.orders orders LEFT JOIN analytics.customers customers ON"
" orders.customer_id=customers.customer_id WHERE DATE_TRUNC('DAY',"
" customers.first_order_date)>DATE_TRUNC('DAY', orders.order_date) GROUP BY orders.new_vs_repeat"
" ORDER BY orders_number_of_orders DESC NULLS LAST;"
)
assert query == correct
2 changes: 1 addition & 1 deletion tests/test_simple_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ def test_simple_query_dimension_filter(connections):


@pytest.mark.query
def test_simple_query_field_to_field_filter(connections):
def test_simple_query_field_on_field_filter(connections):
project = Project(models=[simple_model], views=[simple_view])
conn = MetricsLayerConnection(project=project, connections=connections)
query = conn.get_sql_query(
Expand Down

0 comments on commit 24c10d4

Please sign in to comment.