From b9db730d9598e981c04cfcf15097f649a9aefb3f Mon Sep 17 00:00:00 2001 From: Will Deng Date: Thu, 9 Nov 2023 13:54:32 -0500 Subject: [PATCH] Allow CombineMetricsNode to combine the outputs from aggregated measures --- metricflow/plan_conversion/dataflow_to_sql.py | 33 +- .../test_dataflow_to_sql_plan.py | 64 ++ .../test_combine_output_node__plan0.sql | 234 +++++ ...t_combine_output_node__plan0_optimized.sql | 47 + .../test_combine_output_node__plan0.xml | 858 ++++++++++++++++++ 5 files changed, 1235 insertions(+), 1 deletion(-) create mode 100644 metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0.sql create mode 100644 metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0_optimized.sql create mode 100644 metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/test_combine_output_node__plan0.xml diff --git a/metricflow/plan_conversion/dataflow_to_sql.py b/metricflow/plan_conversion/dataflow_to_sql.py index 66607417fa..2ccebf1aec 100644 --- a/metricflow/plan_conversion/dataflow_to_sql.py +++ b/metricflow/plan_conversion/dataflow_to_sql.py @@ -36,6 +36,7 @@ from metricflow.filters.time_constraint import TimeRangeConstraint from metricflow.instances import ( InstanceSet, + MeasureInstance, MetricInstance, TimeDimensionInstance, ) @@ -787,6 +788,7 @@ def visit_where_constraint_node(self, node: WhereConstraintNode) -> SqlDataSet: def _make_select_columns_for_multiple_metrics( self, table_alias_to_metric_instances: OrderedDict[str, Tuple[MetricInstance, ...]], + table_alias_to_measure_instances: OrderedDict[str, Tuple[MeasureInstance, ...]], aggregation_type: Optional[AggregationType], ) -> List[SqlSelectColumn]: """Creates select columns that get the given metric using the given table alias. @@ -838,6 +840,28 @@ def _make_select_columns_for_multiple_metrics( column_alias=metric_column_name, ) ) + for table_alias, measure_instances in table_alias_to_measure_instances.items(): + for measure_instance in measure_instances: + measure_spec = measure_instance.spec + measure_column_name = self._column_association_resolver.resolve_spec(measure_spec).column_name + column_reference_expression = SqlColumnReferenceExpression( + col_ref=SqlColumnReference( + table_alias=table_alias, + column_name=measure_column_name, + ) + ) + if aggregation_type: + select_expression: SqlExpressionNode = SqlFunctionExpression.build_expression_from_aggregation_type( + aggregation_type=aggregation_type, sql_column_expression=column_reference_expression + ) + else: + select_expression = column_reference_expression + select_columns.append( + SqlSelectColumn( + expr=select_expression, + column_alias=measure_column_name, + ) + ) return select_columns def visit_combine_metrics_node(self, node: CombineMetricsNode) -> SqlDataSet: @@ -877,12 +901,18 @@ def visit_combine_metrics_node(self, node: CombineMetricsNode) -> SqlDataSet: parent_data_sets: List[AnnotatedSqlDataSet] = [] table_alias_to_metric_instances: OrderedDict[str, Tuple[MetricInstance, ...]] = OrderedDict() + table_alias_to_measure_instances: OrderedDict[str, Tuple[MeasureInstance, ...]] = OrderedDict() for parent_node in node.parent_nodes: parent_sql_data_set = parent_node.accept(self) table_alias = self._next_unique_table_alias() parent_data_sets.append(AnnotatedSqlDataSet(data_set=parent_sql_data_set, alias=table_alias)) - table_alias_to_metric_instances[table_alias] = parent_sql_data_set.instance_set.metric_instances + if parent_sql_data_set.instance_set.metric_instances: + table_alias_to_metric_instances[table_alias] = parent_sql_data_set.instance_set.metric_instances + elif parent_sql_data_set.instance_set.measure_instances: + table_alias_to_measure_instances[table_alias] = parent_sql_data_set.instance_set.measure_instances + else: + raise RuntimeError("Attempting to combine output nodes without any measures/metrics upstream") # When we create the components of the join that combines metrics it will be one of INNER, FULL OUTER, # or CROSS JOIN. Order doesn't matter for these join types, so we will use the first element in the FROM @@ -926,6 +956,7 @@ def visit_combine_metrics_node(self, node: CombineMetricsNode) -> SqlDataSet: metric_select_column_set = SelectColumnSet( metric_columns=self._make_select_columns_for_multiple_metrics( table_alias_to_metric_instances=table_alias_to_metric_instances, + table_alias_to_measure_instances=table_alias_to_measure_instances, aggregation_type=metric_aggregation_type, ) ) diff --git a/metricflow/test/plan_conversion/test_dataflow_to_sql_plan.py b/metricflow/test/plan_conversion/test_dataflow_to_sql_plan.py index caebaf4b6d..f95373ad9d 100644 --- a/metricflow/test/plan_conversion/test_dataflow_to_sql_plan.py +++ b/metricflow/test/plan_conversion/test_dataflow_to_sql_plan.py @@ -15,6 +15,7 @@ from metricflow.dataflow.dataflow_plan import ( AggregateMeasuresNode, BaseOutput, + CombineMetricsNode, ComputeMetricsNode, ConstrainTimeRangeNode, DataflowPlan, @@ -1003,3 +1004,66 @@ def test_compute_metrics_node_ratio_from_multiple_semantic_models( sql_client=sql_client, node=dataflow_plan.sink_output_nodes[0].parent_node, ) + + +@pytest.mark.sql_engine_snapshot +def test_combine_output_node( # noqa: D + request: FixtureRequest, + mf_test_session_state: MetricFlowTestSessionState, + dataflow_to_sql_converter: DataflowToSqlQueryPlanConverter, + consistent_id_object_repository: ConsistentIdObjectRepository, + sql_client: SqlClient, +) -> None: + """Tests converting a dataflow plan to a SQL query plan where there is a leaf measure aggregation node. + + Covers SUM, AVERAGE, SUM_BOOLEAN (transformed to SUM upstream), and COUNT_DISTINCT agg types + """ + sum_spec = MeasureSpec( + element_name="bookings", + ) + sum_boolean_spec = MeasureSpec( + element_name="instant_bookings", + ) + count_distinct_spec = MeasureSpec( + element_name="bookers", + ) + dimension_spec = DimensionSpec( + element_name="is_instant", + entity_links=(), + ) + measure_source_node = consistent_id_object_repository.simple_model_read_nodes["bookings_source"] + + # Build compute measures node + measure_specs: List[MeasureSpec] = [sum_spec] + filtered_measure_node = FilterElementsNode( + parent_node=measure_source_node, + include_specs=InstanceSpecSet(measure_specs=tuple(measure_specs), dimension_specs=(dimension_spec,)), + ) + aggregated_measure_node = AggregateMeasuresNode( + parent_node=filtered_measure_node, + metric_input_measure_specs=tuple(MetricInputMeasureSpec(measure_spec=x) for x in measure_specs), + ) + metric_spec = MetricSpec(element_name="bookings") + compute_metrics_node = ComputeMetricsNode(parent_node=aggregated_measure_node, metric_specs=[metric_spec]) + + # Build agg measures node + measure_specs_2 = [sum_boolean_spec, count_distinct_spec] + filtered_measure_node_2 = FilterElementsNode( + parent_node=measure_source_node, + include_specs=InstanceSpecSet(measure_specs=tuple(measure_specs_2), dimension_specs=(dimension_spec,)), + ) + aggregated_measure_node_2 = AggregateMeasuresNode( + parent_node=filtered_measure_node_2, + metric_input_measure_specs=tuple(MetricInputMeasureSpec(measure_spec=x) for x in measure_specs_2), + ) + + # Combine metrics node + combine_metrics_node = CombineMetricsNode([compute_metrics_node, aggregated_measure_node_2]) + + convert_and_check( + request=request, + mf_test_session_state=mf_test_session_state, + dataflow_to_sql_converter=dataflow_to_sql_converter, + sql_client=sql_client, + node=combine_metrics_node, + ) diff --git a/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0.sql b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0.sql new file mode 100644 index 0000000000..58acf2e479 --- /dev/null +++ b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0.sql @@ -0,0 +1,234 @@ +-- Combine Metrics +SELECT + COALESCE(subq_3.is_instant, subq_6.is_instant) AS is_instant + , MAX(subq_3.bookings) AS bookings + , MAX(subq_6.instant_bookings) AS instant_bookings + , MAX(subq_6.bookers) AS bookers +FROM ( + -- Compute Metrics via Expressions + SELECT + subq_2.is_instant + , subq_2.bookings + FROM ( + -- Aggregate Measures + SELECT + subq_1.is_instant + , SUM(subq_1.bookings) AS bookings + FROM ( + -- Pass Only Elements: + -- ['bookings', 'is_instant'] + SELECT + subq_0.is_instant + , subq_0.bookings + FROM ( + -- Read Elements From Semantic Model 'bookings_source' + SELECT + 1 AS bookings + , CASE WHEN is_instant THEN 1 ELSE 0 END AS instant_bookings + , bookings_source_src_10001.booking_value + , bookings_source_src_10001.booking_value AS max_booking_value + , bookings_source_src_10001.booking_value AS min_booking_value + , bookings_source_src_10001.guest_id AS bookers + , bookings_source_src_10001.booking_value AS average_booking_value + , bookings_source_src_10001.booking_value AS booking_payments + , CASE WHEN referrer_id IS NOT NULL THEN 1 ELSE 0 END AS referred_bookings + , bookings_source_src_10001.booking_value AS median_booking_value + , bookings_source_src_10001.booking_value AS booking_value_p99 + , bookings_source_src_10001.booking_value AS discrete_booking_value_p99 + , bookings_source_src_10001.booking_value AS approximate_continuous_booking_value_p99 + , bookings_source_src_10001.booking_value AS approximate_discrete_booking_value_p99 + , bookings_source_src_10001.is_instant + , DATE_TRUNC('day', bookings_source_src_10001.ds) AS ds__day + , DATE_TRUNC('week', bookings_source_src_10001.ds) AS ds__week + , DATE_TRUNC('month', bookings_source_src_10001.ds) AS ds__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds) AS ds__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds) AS ds__year + , EXTRACT(year FROM bookings_source_src_10001.ds) AS ds__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds) AS ds__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds) AS ds__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds) AS ds__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds) AS ds__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds) AS ds__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__day + , DATE_TRUNC('week', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__week + , DATE_TRUNC('month', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__year + , EXTRACT(year FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.paid_at) AS paid_at__day + , DATE_TRUNC('week', bookings_source_src_10001.paid_at) AS paid_at__week + , DATE_TRUNC('month', bookings_source_src_10001.paid_at) AS paid_at__month + , DATE_TRUNC('quarter', bookings_source_src_10001.paid_at) AS paid_at__quarter + , DATE_TRUNC('year', bookings_source_src_10001.paid_at) AS paid_at__year + , EXTRACT(year FROM bookings_source_src_10001.paid_at) AS paid_at__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.paid_at) AS paid_at__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.paid_at) AS paid_at__extract_month + , EXTRACT(day FROM bookings_source_src_10001.paid_at) AS paid_at__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.paid_at) AS paid_at__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.paid_at) AS paid_at__extract_doy + , bookings_source_src_10001.is_instant AS booking__is_instant + , DATE_TRUNC('day', bookings_source_src_10001.ds) AS booking__ds__day + , DATE_TRUNC('week', bookings_source_src_10001.ds) AS booking__ds__week + , DATE_TRUNC('month', bookings_source_src_10001.ds) AS booking__ds__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds) AS booking__ds__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds) AS booking__ds__year + , EXTRACT(year FROM bookings_source_src_10001.ds) AS booking__ds__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds) AS booking__ds__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds) AS booking__ds__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds) AS booking__ds__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds) AS booking__ds__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds) AS booking__ds__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__day + , DATE_TRUNC('week', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__week + , DATE_TRUNC('month', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__year + , EXTRACT(year FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.paid_at) AS booking__paid_at__day + , DATE_TRUNC('week', bookings_source_src_10001.paid_at) AS booking__paid_at__week + , DATE_TRUNC('month', bookings_source_src_10001.paid_at) AS booking__paid_at__month + , DATE_TRUNC('quarter', bookings_source_src_10001.paid_at) AS booking__paid_at__quarter + , DATE_TRUNC('year', bookings_source_src_10001.paid_at) AS booking__paid_at__year + , EXTRACT(year FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_month + , EXTRACT(day FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_doy + , bookings_source_src_10001.listing_id AS listing + , bookings_source_src_10001.guest_id AS guest + , bookings_source_src_10001.host_id AS host + , bookings_source_src_10001.listing_id AS booking__listing + , bookings_source_src_10001.guest_id AS booking__guest + , bookings_source_src_10001.host_id AS booking__host + FROM ***************************.fct_bookings bookings_source_src_10001 + ) subq_0 + ) subq_1 + GROUP BY + subq_1.is_instant + ) subq_2 +) subq_3 +FULL OUTER JOIN ( + -- Aggregate Measures + SELECT + subq_5.is_instant + , SUM(subq_5.instant_bookings) AS instant_bookings + , COUNT(DISTINCT subq_5.bookers) AS bookers + FROM ( + -- Pass Only Elements: + -- ['instant_bookings', 'bookers', 'is_instant'] + SELECT + subq_4.is_instant + , subq_4.instant_bookings + , subq_4.bookers + FROM ( + -- Read Elements From Semantic Model 'bookings_source' + SELECT + 1 AS bookings + , CASE WHEN is_instant THEN 1 ELSE 0 END AS instant_bookings + , bookings_source_src_10001.booking_value + , bookings_source_src_10001.booking_value AS max_booking_value + , bookings_source_src_10001.booking_value AS min_booking_value + , bookings_source_src_10001.guest_id AS bookers + , bookings_source_src_10001.booking_value AS average_booking_value + , bookings_source_src_10001.booking_value AS booking_payments + , CASE WHEN referrer_id IS NOT NULL THEN 1 ELSE 0 END AS referred_bookings + , bookings_source_src_10001.booking_value AS median_booking_value + , bookings_source_src_10001.booking_value AS booking_value_p99 + , bookings_source_src_10001.booking_value AS discrete_booking_value_p99 + , bookings_source_src_10001.booking_value AS approximate_continuous_booking_value_p99 + , bookings_source_src_10001.booking_value AS approximate_discrete_booking_value_p99 + , bookings_source_src_10001.is_instant + , DATE_TRUNC('day', bookings_source_src_10001.ds) AS ds__day + , DATE_TRUNC('week', bookings_source_src_10001.ds) AS ds__week + , DATE_TRUNC('month', bookings_source_src_10001.ds) AS ds__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds) AS ds__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds) AS ds__year + , EXTRACT(year FROM bookings_source_src_10001.ds) AS ds__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds) AS ds__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds) AS ds__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds) AS ds__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds) AS ds__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds) AS ds__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__day + , DATE_TRUNC('week', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__week + , DATE_TRUNC('month', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds_partitioned) AS ds_partitioned__year + , EXTRACT(year FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds_partitioned) AS ds_partitioned__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.paid_at) AS paid_at__day + , DATE_TRUNC('week', bookings_source_src_10001.paid_at) AS paid_at__week + , DATE_TRUNC('month', bookings_source_src_10001.paid_at) AS paid_at__month + , DATE_TRUNC('quarter', bookings_source_src_10001.paid_at) AS paid_at__quarter + , DATE_TRUNC('year', bookings_source_src_10001.paid_at) AS paid_at__year + , EXTRACT(year FROM bookings_source_src_10001.paid_at) AS paid_at__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.paid_at) AS paid_at__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.paid_at) AS paid_at__extract_month + , EXTRACT(day FROM bookings_source_src_10001.paid_at) AS paid_at__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.paid_at) AS paid_at__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.paid_at) AS paid_at__extract_doy + , bookings_source_src_10001.is_instant AS booking__is_instant + , DATE_TRUNC('day', bookings_source_src_10001.ds) AS booking__ds__day + , DATE_TRUNC('week', bookings_source_src_10001.ds) AS booking__ds__week + , DATE_TRUNC('month', bookings_source_src_10001.ds) AS booking__ds__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds) AS booking__ds__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds) AS booking__ds__year + , EXTRACT(year FROM bookings_source_src_10001.ds) AS booking__ds__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds) AS booking__ds__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds) AS booking__ds__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds) AS booking__ds__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds) AS booking__ds__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds) AS booking__ds__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__day + , DATE_TRUNC('week', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__week + , DATE_TRUNC('month', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__month + , DATE_TRUNC('quarter', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__quarter + , DATE_TRUNC('year', bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__year + , EXTRACT(year FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_month + , EXTRACT(day FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.ds_partitioned) AS booking__ds_partitioned__extract_doy + , DATE_TRUNC('day', bookings_source_src_10001.paid_at) AS booking__paid_at__day + , DATE_TRUNC('week', bookings_source_src_10001.paid_at) AS booking__paid_at__week + , DATE_TRUNC('month', bookings_source_src_10001.paid_at) AS booking__paid_at__month + , DATE_TRUNC('quarter', bookings_source_src_10001.paid_at) AS booking__paid_at__quarter + , DATE_TRUNC('year', bookings_source_src_10001.paid_at) AS booking__paid_at__year + , EXTRACT(year FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_year + , EXTRACT(quarter FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_quarter + , EXTRACT(month FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_month + , EXTRACT(day FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_day + , EXTRACT(isodow FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_dow + , EXTRACT(doy FROM bookings_source_src_10001.paid_at) AS booking__paid_at__extract_doy + , bookings_source_src_10001.listing_id AS listing + , bookings_source_src_10001.guest_id AS guest + , bookings_source_src_10001.host_id AS host + , bookings_source_src_10001.listing_id AS booking__listing + , bookings_source_src_10001.guest_id AS booking__guest + , bookings_source_src_10001.host_id AS booking__host + FROM ***************************.fct_bookings bookings_source_src_10001 + ) subq_4 + ) subq_5 + GROUP BY + subq_5.is_instant +) subq_6 +ON + subq_3.is_instant = subq_6.is_instant +GROUP BY + COALESCE(subq_3.is_instant, subq_6.is_instant) diff --git a/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0_optimized.sql b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0_optimized.sql new file mode 100644 index 0000000000..fa10c69ca0 --- /dev/null +++ b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/DuckDB/test_combine_output_node__plan0_optimized.sql @@ -0,0 +1,47 @@ +-- Combine Metrics +SELECT + COALESCE(subq_10.is_instant, subq_13.is_instant) AS is_instant + , MAX(subq_10.bookings) AS bookings + , MAX(subq_13.instant_bookings) AS instant_bookings + , MAX(subq_13.bookers) AS bookers +FROM ( + -- Aggregate Measures + -- Compute Metrics via Expressions + SELECT + is_instant + , SUM(bookings) AS bookings + FROM ( + -- Read Elements From Semantic Model 'bookings_source' + -- Pass Only Elements: + -- ['bookings', 'is_instant'] + SELECT + is_instant + , 1 AS bookings + FROM ***************************.fct_bookings bookings_source_src_10001 + ) subq_8 + GROUP BY + is_instant +) subq_10 +FULL OUTER JOIN ( + -- Aggregate Measures + SELECT + is_instant + , SUM(instant_bookings) AS instant_bookings + , COUNT(DISTINCT bookers) AS bookers + FROM ( + -- Read Elements From Semantic Model 'bookings_source' + -- Pass Only Elements: + -- ['instant_bookings', 'bookers', 'is_instant'] + SELECT + is_instant + , CASE WHEN is_instant THEN 1 ELSE 0 END AS instant_bookings + , guest_id AS bookers + FROM ***************************.fct_bookings bookings_source_src_10001 + ) subq_12 + GROUP BY + is_instant +) subq_13 +ON + subq_10.is_instant = subq_13.is_instant +GROUP BY + COALESCE(subq_10.is_instant, subq_13.is_instant) diff --git a/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/test_combine_output_node__plan0.xml b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/test_combine_output_node__plan0.xml new file mode 100644 index 0000000000..138a7e2250 --- /dev/null +++ b/metricflow/test/snapshots/test_dataflow_to_sql_plan.py/SqlQueryPlan/test_combine_output_node__plan0.xml @@ -0,0 +1,858 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +