mirror of
https://github.com/apache/superset.git
synced 2026-09-01 04:51:23 +00:00
284 lines
9.0 KiB
Python
284 lines
9.0 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
from typing import cast
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from superset.common import query_actions
|
|
from superset.common.chart_data import ChartDataResultType
|
|
from superset.common.chart_data_timing import (
|
|
QueryAcquisitionResult,
|
|
QueryAcquisitionTiming,
|
|
)
|
|
from superset.common.query_actions import (
|
|
_prepare_drill_detail_query,
|
|
_prepare_samples_query,
|
|
get_query_results,
|
|
get_query_results_with_timing,
|
|
)
|
|
from superset.common.query_object import QueryObject
|
|
from superset.exceptions import QueryObjectValidationError
|
|
from superset.utils.core import QueryObjectFilterClause
|
|
|
|
|
|
def test_prepare_drill_detail_query_does_not_strip_filters() -> None:
|
|
"""Drill preparation keeps filters while rewriting the row query shape."""
|
|
applied_filter: QueryObjectFilterClause = {
|
|
"col": "region",
|
|
"op": "==",
|
|
"val": "USA",
|
|
}
|
|
query_obj = QueryObject(
|
|
columns=["region", "sales"],
|
|
metrics=["count"],
|
|
filters=[applied_filter],
|
|
)
|
|
|
|
datasource = MagicMock()
|
|
datasource.columns = [
|
|
MagicMock(column_name="region"),
|
|
MagicMock(column_name="sales"),
|
|
]
|
|
query_context = MagicMock()
|
|
query_context.datasource = datasource
|
|
|
|
prepared = _prepare_drill_detail_query(query_context, query_obj)
|
|
|
|
assert applied_filter in prepared.filter
|
|
|
|
|
|
def test_prepare_samples_query_marks_query_as_system_sampling() -> None:
|
|
"""Sample preparation is isolated from the caller's query object."""
|
|
query_obj = QueryObject(columns=["region"], metrics=["count"])
|
|
original_extras = query_obj.extras
|
|
datasource = MagicMock()
|
|
datasource.columns = [MagicMock(column_name="region")]
|
|
query_context = MagicMock()
|
|
query_context.datasource = datasource
|
|
|
|
prepared = _prepare_samples_query(query_context, query_obj)
|
|
|
|
assert prepared.extras.get("system_sampling") is True
|
|
assert "system_sampling" not in query_obj.extras
|
|
assert query_obj.extras is original_extras
|
|
|
|
|
|
def test_timed_dataframe_result_uses_sidecar_and_continuous_total() -> None:
|
|
query_context = MagicMock()
|
|
query_obj = MagicMock()
|
|
acquisition_timing = QueryAcquisitionTiming(
|
|
query_planning_ns=1,
|
|
cache_resolution_ns=2,
|
|
data_acquisition_ns=3,
|
|
payload_assembly_ns=4,
|
|
)
|
|
query_context.get_df_payload_result.return_value = QueryAcquisitionResult(
|
|
payload={"df": "frame"},
|
|
timing=acquisition_timing,
|
|
)
|
|
|
|
with (
|
|
patch(
|
|
"superset.common.query_actions._materialize_full_payload",
|
|
return_value={"data": [{"col1": 1}]},
|
|
) as materialize_full_payload,
|
|
patch(
|
|
"superset.common.query_actions.time.perf_counter_ns",
|
|
side_effect=[100, 110, 120, 150],
|
|
),
|
|
):
|
|
result = get_query_results_with_timing(
|
|
ChartDataResultType.FULL,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
|
|
query_context.get_df_payload_result.assert_called_once_with(
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
query_context.get_df_payload.assert_not_called()
|
|
materialize_full_payload.assert_called_once_with(
|
|
query_context,
|
|
query_obj,
|
|
{"df": "frame"},
|
|
)
|
|
assert result.payload == {"data": [{"col1": 1}]}
|
|
assert result.timing.query_planning_ns == 1
|
|
assert result.timing.cache_resolution_ns == 2
|
|
assert result.timing.data_acquisition_ns == 3
|
|
assert result.timing.payload_assembly_ns == 14
|
|
assert result.timing.total_ns == 50
|
|
|
|
|
|
def test_metadata_result_has_null_phases_and_numeric_total() -> None:
|
|
query_context = MagicMock()
|
|
query_obj = MagicMock()
|
|
result_func = MagicMock(return_value={"language": "sql", "query": "SELECT 1"})
|
|
|
|
with (
|
|
patch.dict(
|
|
"superset.common.query_actions._metadata_result_type_functions",
|
|
{ChartDataResultType.QUERY: result_func},
|
|
clear=True,
|
|
),
|
|
patch(
|
|
"superset.common.query_actions.time.perf_counter_ns",
|
|
side_effect=[100, 125],
|
|
),
|
|
):
|
|
result = get_query_results_with_timing(
|
|
ChartDataResultType.QUERY,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=True,
|
|
)
|
|
|
|
result_func.assert_called_once_with(query_context, query_obj, True)
|
|
assert result.payload == {"language": "sql", "query": "SELECT 1"}
|
|
assert result.timing.query_planning_ns is None
|
|
assert result.timing.cache_resolution_ns is None
|
|
assert result.timing.data_acquisition_ns is None
|
|
assert result.timing.payload_assembly_ns is None
|
|
assert result.timing.total_ns == 25
|
|
|
|
|
|
def test_result_type_dispatchers_are_complete_and_disjoint() -> None:
|
|
"""Every result type is owned by exactly one timing execution path."""
|
|
metadata_types = set(query_actions._metadata_result_type_functions)
|
|
data_types = set(query_actions._data_result_type_preparers)
|
|
|
|
assert metadata_types.isdisjoint(data_types)
|
|
assert metadata_types | data_types == set(ChartDataResultType)
|
|
|
|
|
|
def test_timed_result_refuses_a_result_type_without_a_dispatch_owner() -> None:
|
|
with pytest.raises(QueryObjectValidationError, match="Invalid result type"):
|
|
get_query_results_with_timing(
|
|
cast("ChartDataResultType", "unowned"),
|
|
MagicMock(),
|
|
MagicMock(),
|
|
force_cached=False,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"result_type",
|
|
[ChartDataResultType.SAMPLES, ChartDataResultType.DRILL_DETAIL],
|
|
)
|
|
def test_data_preparation_is_inside_the_continuous_total(
|
|
result_type: ChartDataResultType,
|
|
) -> None:
|
|
query_context = MagicMock()
|
|
query_obj = MagicMock()
|
|
preparation_started_ns: list[int] = []
|
|
acquisition_timing = QueryAcquisitionTiming(
|
|
query_planning_ns=1,
|
|
cache_resolution_ns=2,
|
|
data_acquisition_ns=3,
|
|
payload_assembly_ns=4,
|
|
)
|
|
|
|
def preparer(_query_context: MagicMock, prepared_query: MagicMock) -> MagicMock:
|
|
preparation_started_ns.append(query_actions.time.perf_counter_ns())
|
|
return prepared_query
|
|
|
|
with (
|
|
patch.dict(
|
|
"superset.common.query_actions._data_result_type_preparers",
|
|
{result_type: preparer},
|
|
),
|
|
patch(
|
|
"superset.common.query_actions._get_full_with_timing",
|
|
return_value=(
|
|
{"data": []},
|
|
acquisition_timing,
|
|
5,
|
|
),
|
|
),
|
|
patch(
|
|
"superset.common.query_actions.time.perf_counter_ns",
|
|
side_effect=[100, 125, 150],
|
|
),
|
|
):
|
|
result = get_query_results_with_timing(
|
|
result_type,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
|
|
assert preparation_started_ns == [125]
|
|
assert result.timing.total_ns == 50
|
|
|
|
|
|
def test_timed_drill_detail_keeps_capability_refusal() -> None:
|
|
query_context = MagicMock()
|
|
query_context.datasource = MagicMock(supports_drill_to_detail=False)
|
|
query_obj = MagicMock()
|
|
query_obj.datasource = None
|
|
|
|
with pytest.raises(QueryObjectValidationError):
|
|
get_query_results_with_timing(
|
|
ChartDataResultType.DRILL_DETAIL,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
|
|
|
|
def test_legacy_result_wrapper_keeps_drill_detail_capability_refusal() -> None:
|
|
query_context = MagicMock()
|
|
query_context.datasource = MagicMock(supports_drill_to_detail=False)
|
|
query_obj = MagicMock()
|
|
query_obj.datasource = None
|
|
|
|
with pytest.raises(QueryObjectValidationError):
|
|
get_query_results(
|
|
ChartDataResultType.DRILL_DETAIL,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
|
|
|
|
def test_legacy_result_wrapper_delegates_to_timed_resolver() -> None:
|
|
query_context = MagicMock()
|
|
query_obj = MagicMock()
|
|
|
|
with patch(
|
|
"superset.common.query_actions.get_query_results_with_timing"
|
|
) as timed_resolver:
|
|
timed_resolver.return_value.payload = {"data": []}
|
|
|
|
result = get_query_results(
|
|
ChartDataResultType.FULL,
|
|
query_context,
|
|
query_obj,
|
|
force_cached=False,
|
|
)
|
|
|
|
assert result == {"data": []}
|
|
timed_resolver.assert_called_once_with(
|
|
ChartDataResultType.FULL,
|
|
query_context,
|
|
query_obj,
|
|
False,
|
|
)
|