mirror of
https://github.com/apache/superset.git
synced 2026-05-15 04:45:10 +00:00
Compare commits
21 Commits
fix/mcp-li
...
refactor_q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
398842a4d8 | ||
|
|
aff7f54b1a | ||
|
|
288da4a050 | ||
|
|
2d8ae42d42 | ||
|
|
990174bb1c | ||
|
|
6f8a79693d | ||
|
|
b8a71e4754 | ||
|
|
13e7ba18ed | ||
|
|
c5887630ab | ||
|
|
c11efecdad | ||
|
|
3dc97b11f8 | ||
|
|
b81487e177 | ||
|
|
72e33ba811 | ||
|
|
b0715bd8bb | ||
|
|
0348b6c313 | ||
|
|
453b3da9f6 | ||
|
|
7c6c0c0451 | ||
|
|
bf43704200 | ||
|
|
fef0676954 | ||
|
|
7485af5e6c | ||
|
|
825b9e784a |
@@ -145,7 +145,7 @@ def _get_samples(
|
|||||||
query_obj = copy.copy(query_obj)
|
query_obj = copy.copy(query_obj)
|
||||||
query_obj.is_timeseries = False
|
query_obj.is_timeseries = False
|
||||||
query_obj.orderby = []
|
query_obj.orderby = []
|
||||||
query_obj.metrics = None
|
query_obj.metrics = []
|
||||||
query_obj.post_processing = []
|
query_obj.post_processing = []
|
||||||
qry_obj_cols = []
|
qry_obj_cols = []
|
||||||
for o in datasource.columns:
|
for o in datasource.columns:
|
||||||
@@ -168,7 +168,7 @@ def _get_drill_detail(
|
|||||||
query_obj = copy.copy(query_obj)
|
query_obj = copy.copy(query_obj)
|
||||||
query_obj.is_timeseries = False
|
query_obj.is_timeseries = False
|
||||||
query_obj.orderby = []
|
query_obj.orderby = []
|
||||||
query_obj.metrics = None
|
query_obj.metrics = []
|
||||||
query_obj.post_processing = []
|
query_obj.post_processing = []
|
||||||
qry_obj_cols = []
|
qry_obj_cols = []
|
||||||
for o in datasource.columns:
|
for o in datasource.columns:
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
apply_fetch_values_predicate: bool
|
apply_fetch_values_predicate: bool
|
||||||
columns: list[Column]
|
columns: list[Column]
|
||||||
datasource: BaseDatasource | None
|
datasource: BaseDatasource | None
|
||||||
|
columns_by_name: dict[str, Any]
|
||||||
|
metrics_by_name: dict[str, Any]
|
||||||
extras: dict[str, Any]
|
extras: dict[str, Any]
|
||||||
filter: list[QueryObjectFilterClause]
|
filter: list[QueryObjectFilterClause]
|
||||||
from_dttm: datetime | None
|
from_dttm: datetime | None
|
||||||
@@ -94,7 +96,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
inner_to_dttm: datetime | None
|
inner_to_dttm: datetime | None
|
||||||
is_rowcount: bool
|
is_rowcount: bool
|
||||||
is_timeseries: bool
|
is_timeseries: bool
|
||||||
metrics: list[Metric] | None
|
metrics: list[Metric]
|
||||||
order_desc: bool
|
order_desc: bool
|
||||||
orderby: list[OrderBy]
|
orderby: list[OrderBy]
|
||||||
post_processing: list[dict[str, Any]]
|
post_processing: list[dict[str, Any]]
|
||||||
@@ -141,6 +143,30 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
self.apply_fetch_values_predicate = apply_fetch_values_predicate or False
|
self.apply_fetch_values_predicate = apply_fetch_values_predicate or False
|
||||||
self.columns = columns or []
|
self.columns = columns or []
|
||||||
self.datasource = datasource
|
self.datasource = datasource
|
||||||
|
|
||||||
|
# Build datasource mappings for easy lookup
|
||||||
|
self.columns_by_name: dict[str, Any] = {}
|
||||||
|
self.metrics_by_name: dict[str, Any] = {}
|
||||||
|
|
||||||
|
if datasource:
|
||||||
|
try:
|
||||||
|
if hasattr(datasource, "columns") and datasource.columns is not None:
|
||||||
|
self.columns_by_name = {
|
||||||
|
col.column_name: col for col in datasource.columns
|
||||||
|
}
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# Handle mocked datasources or other non-iterable cases
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
if hasattr(datasource, "metrics") and datasource.metrics is not None:
|
||||||
|
self.metrics_by_name = {
|
||||||
|
metric.metric_name: metric for metric in datasource.metrics
|
||||||
|
}
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# Handle mocked datasources or other non-iterable cases
|
||||||
|
pass
|
||||||
|
|
||||||
self.extras = extras or {}
|
self.extras = extras or {}
|
||||||
self.filter = filters or []
|
self.filter = filters or []
|
||||||
self.granularity = granularity
|
self.granularity = granularity
|
||||||
@@ -192,9 +218,12 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
def is_str_or_adhoc(metric: Metric) -> bool:
|
def is_str_or_adhoc(metric: Metric) -> bool:
|
||||||
return isinstance(metric, str) or is_adhoc_metric(metric)
|
return isinstance(metric, str) or is_adhoc_metric(metric)
|
||||||
|
|
||||||
self.metrics = metrics and [
|
# Track whether metrics was originally None (for need_groupby logic)
|
||||||
|
self._metrics_is_not_none = metrics is not None
|
||||||
|
|
||||||
|
self.metrics = [
|
||||||
x if is_str_or_adhoc(x) else x["label"] # type: ignore
|
x if is_str_or_adhoc(x) else x["label"] # type: ignore
|
||||||
for x in metrics
|
for x in (metrics or [])
|
||||||
]
|
]
|
||||||
|
|
||||||
def _set_post_processing(
|
def _set_post_processing(
|
||||||
@@ -226,15 +255,20 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
field.new_name,
|
field.new_name,
|
||||||
)
|
)
|
||||||
value = kwargs[field.old_name]
|
value = kwargs[field.old_name]
|
||||||
if value:
|
if value is not None:
|
||||||
if hasattr(self, field.new_name):
|
# Only override if the new field is not already populated with data
|
||||||
|
current_value = getattr(self, field.new_name, None)
|
||||||
|
if (
|
||||||
|
current_value
|
||||||
|
): # If field already has truthy data, don't override
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"The field `%s` is already populated, "
|
"The field `%s` is already populated, "
|
||||||
"replacing value with contents from `%s`.",
|
"not replacing with contents from deprecated `%s`.",
|
||||||
field.new_name,
|
field.new_name,
|
||||||
field.old_name,
|
field.old_name,
|
||||||
)
|
)
|
||||||
setattr(self, field.new_name, value)
|
else:
|
||||||
|
setattr(self, field.new_name, value)
|
||||||
|
|
||||||
def _move_deprecated_extra_fields(self, kwargs: dict[str, Any]) -> None:
|
def _move_deprecated_extra_fields(self, kwargs: dict[str, Any]) -> None:
|
||||||
# move deprecated extras fields to extras
|
# move deprecated extras fields to extras
|
||||||
@@ -247,8 +281,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
field.new_name,
|
field.new_name,
|
||||||
)
|
)
|
||||||
value = kwargs[field.old_name]
|
value = kwargs[field.old_name]
|
||||||
if value:
|
if value is not None and value != "": # Don't add empty string values
|
||||||
if hasattr(self.extras, field.new_name):
|
if field.new_name in self.extras:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"The field `%s` is already populated in "
|
"The field `%s` is already populated in "
|
||||||
"`extras`, replacing value with contents "
|
"`extras`, replacing value with contents "
|
||||||
@@ -262,7 +296,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
def metric_names(self) -> list[str]:
|
def metric_names(self) -> list[str]:
|
||||||
"""Return metrics names (labels), coerce adhoc metrics to strings."""
|
"""Return metrics names (labels), coerce adhoc metrics to strings."""
|
||||||
return get_metric_names(
|
return get_metric_names(
|
||||||
self.metrics or [],
|
self.metrics,
|
||||||
(
|
(
|
||||||
self.datasource.verbose_map
|
self.datasource.verbose_map
|
||||||
if self.datasource and hasattr(self.datasource, "verbose_map")
|
if self.datasource and hasattr(self.datasource, "verbose_map")
|
||||||
@@ -276,6 +310,428 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
and metrics are non-empty, otherwise returns column labels."""
|
and metrics are non-empty, otherwise returns column labels."""
|
||||||
return get_column_names(self.columns)
|
return get_column_names(self.columns)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_grain(self) -> str | None:
|
||||||
|
"""Get time grain from extras."""
|
||||||
|
return (self.extras or {}).get("time_grain_sqla")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def need_groupby(self) -> bool:
|
||||||
|
"""Determine if GROUP BY is needed based on metrics and columns."""
|
||||||
|
# GROUP BY is needed when there are metrics or when metrics is explicitly
|
||||||
|
# provided (even as empty list). When metrics=None, columns are just for
|
||||||
|
# selection without aggregation, so no GROUP BY needed.
|
||||||
|
return self._metrics_is_not_none
|
||||||
|
|
||||||
|
@property
|
||||||
|
def groupby(self) -> list[Column]:
|
||||||
|
"""Alias for columns (for backward compatibility/clarity)."""
|
||||||
|
return self.columns or []
|
||||||
|
|
||||||
|
def get_series_limit_prequery_obj(
|
||||||
|
self,
|
||||||
|
granularity: str | None,
|
||||||
|
inner_from_dttm: datetime | None,
|
||||||
|
inner_to_dttm: datetime | None,
|
||||||
|
orderby: list[OrderBy] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Build prequery object for series limit queries.
|
||||||
|
|
||||||
|
This is used to determine top groups when series_limit is set.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
granularity: The time column name
|
||||||
|
inner_from_dttm: Inner from datetime (if different from main query)
|
||||||
|
inner_to_dttm: Inner to datetime (if different from main query)
|
||||||
|
orderby: Optional orderby to override (for series_limit_metric)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary suitable for passing to query()
|
||||||
|
"""
|
||||||
|
from superset.utils.core import get_non_base_axis_columns
|
||||||
|
|
||||||
|
return {
|
||||||
|
"is_timeseries": False,
|
||||||
|
"row_limit": self.series_limit,
|
||||||
|
"metrics": self.metrics,
|
||||||
|
"granularity": granularity,
|
||||||
|
"groupby": self.groupby,
|
||||||
|
"from_dttm": inner_from_dttm or self.from_dttm,
|
||||||
|
"to_dttm": inner_to_dttm or self.to_dttm,
|
||||||
|
"filter": self.filter,
|
||||||
|
"orderby": orderby or [],
|
||||||
|
"extras": self.extras or {},
|
||||||
|
"columns": get_non_base_axis_columns(self.columns),
|
||||||
|
"order_desc": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_select_expressions( # noqa: C901
|
||||||
|
self,
|
||||||
|
granularity: str | None,
|
||||||
|
series_column_labels: set[str],
|
||||||
|
datasource: Any, # BaseDatasource
|
||||||
|
template_processor: Any,
|
||||||
|
) -> tuple[list[Any], dict[str, Any], dict[str, Any]]:
|
||||||
|
"""Build SELECT expressions for the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
granularity: The time column name
|
||||||
|
series_column_labels: Labels of series columns
|
||||||
|
datasource: The datasource being queried
|
||||||
|
template_processor: Template processor for SQL templating
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (select_exprs, groupby_all_columns, groupby_series_columns)
|
||||||
|
"""
|
||||||
|
from sqlalchemy import literal_column
|
||||||
|
|
||||||
|
from superset.utils.core import (
|
||||||
|
DTTM_ALIAS,
|
||||||
|
is_adhoc_column,
|
||||||
|
)
|
||||||
|
|
||||||
|
select_exprs = []
|
||||||
|
groupby_all_columns = {}
|
||||||
|
groupby_series_columns = {}
|
||||||
|
|
||||||
|
# Filter out the pseudo column __timestamp from columns
|
||||||
|
columns = [col for col in self.columns if col != DTTM_ALIAS]
|
||||||
|
|
||||||
|
if self.need_groupby:
|
||||||
|
# dedup columns while preserving order
|
||||||
|
columns = self.groupby or self.columns
|
||||||
|
for selected in columns:
|
||||||
|
if isinstance(selected, str):
|
||||||
|
# if groupby field/expr equals granularity field/expr
|
||||||
|
if selected == granularity:
|
||||||
|
table_col = self.columns_by_name[selected]
|
||||||
|
outer = table_col.get_timestamp_expression(
|
||||||
|
time_grain=self.time_grain,
|
||||||
|
label=selected,
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
# if groupby field equals a selected column
|
||||||
|
elif selected in self.columns_by_name:
|
||||||
|
outer = datasource.convert_tbl_column_to_sqla_col(
|
||||||
|
self.columns_by_name[selected],
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from superset.models.helpers import validate_adhoc_subquery
|
||||||
|
|
||||||
|
selected = validate_adhoc_subquery(
|
||||||
|
selected,
|
||||||
|
datasource.database,
|
||||||
|
datasource.catalog,
|
||||||
|
datasource.schema,
|
||||||
|
datasource.database.db_engine_spec.engine,
|
||||||
|
)
|
||||||
|
outer = literal_column(f"({selected})")
|
||||||
|
outer = datasource.make_sqla_column_compatible(outer, selected)
|
||||||
|
else:
|
||||||
|
outer = datasource.adhoc_column_to_sqla(
|
||||||
|
col=selected,
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
groupby_all_columns[outer.name] = outer
|
||||||
|
if (
|
||||||
|
self.is_timeseries and not series_column_labels
|
||||||
|
) or outer.name in series_column_labels:
|
||||||
|
groupby_series_columns[outer.name] = outer
|
||||||
|
select_exprs.append(outer)
|
||||||
|
elif self.columns:
|
||||||
|
with datasource.database.get_sqla_engine() as engine:
|
||||||
|
quote = engine.dialect.identifier_preparer.quote
|
||||||
|
|
||||||
|
for selected in self.columns:
|
||||||
|
if is_adhoc_column(selected):
|
||||||
|
_sql = selected["sqlExpression"]
|
||||||
|
_column_label = selected["label"]
|
||||||
|
elif isinstance(selected, str):
|
||||||
|
_sql = quote(selected)
|
||||||
|
_column_label = selected
|
||||||
|
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from superset.models.helpers import validate_adhoc_subquery
|
||||||
|
|
||||||
|
selected = validate_adhoc_subquery(
|
||||||
|
_sql,
|
||||||
|
datasource.database,
|
||||||
|
datasource.catalog,
|
||||||
|
datasource.schema,
|
||||||
|
datasource.database.db_engine_spec.engine,
|
||||||
|
)
|
||||||
|
|
||||||
|
select_exprs.append(
|
||||||
|
datasource.convert_tbl_column_to_sqla_col(
|
||||||
|
self.columns_by_name[selected],
|
||||||
|
template_processor=template_processor,
|
||||||
|
label=_column_label,
|
||||||
|
)
|
||||||
|
if selected in self.columns_by_name
|
||||||
|
else datasource.make_sqla_column_compatible(
|
||||||
|
literal_column(selected), _column_label
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return select_exprs, groupby_all_columns, groupby_series_columns
|
||||||
|
|
||||||
|
def build_filter_clauses( # noqa: C901
|
||||||
|
self,
|
||||||
|
datasource: Any, # BaseDatasource
|
||||||
|
template_processor: Any,
|
||||||
|
time_filters: list[Any],
|
||||||
|
removed_filters: set[str],
|
||||||
|
applied_adhoc_filters_columns: list[Any],
|
||||||
|
rejected_adhoc_filters_columns: list[Any],
|
||||||
|
is_timeseries: bool,
|
||||||
|
dttm_col: Any,
|
||||||
|
) -> tuple[list[Any], list[Any]]:
|
||||||
|
"""Build WHERE and HAVING filter clauses for the query.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
datasource: The datasource being queried
|
||||||
|
template_processor: Template processor for SQL templating
|
||||||
|
time_filters: Time-based filters to apply
|
||||||
|
removed_filters: Set of filter column names handled by Jinja templates
|
||||||
|
applied_adhoc_filters_columns: List to track applied adhoc filters
|
||||||
|
rejected_adhoc_filters_columns: List to track rejected adhoc filters
|
||||||
|
is_timeseries: Whether this is a timeseries query
|
||||||
|
dttm_col: The datetime column object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (where_clause_and, having_clause_and)
|
||||||
|
"""
|
||||||
|
from flask import current_app
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from superset import feature_flag_manager
|
||||||
|
from superset.common.utils.time_range_utils import (
|
||||||
|
get_since_until_from_time_range,
|
||||||
|
)
|
||||||
|
from superset.exceptions import QueryObjectValidationError
|
||||||
|
from superset.utils.core import (
|
||||||
|
DTTM_ALIAS,
|
||||||
|
FilterOperator,
|
||||||
|
GenericDataType,
|
||||||
|
get_column_name,
|
||||||
|
is_adhoc_column,
|
||||||
|
)
|
||||||
|
|
||||||
|
where_clause_and = []
|
||||||
|
having_clause_and = []
|
||||||
|
|
||||||
|
# Process regular filters
|
||||||
|
for flt in self.filter:
|
||||||
|
if not all(flt.get(s) for s in ["col", "op"]):
|
||||||
|
continue
|
||||||
|
flt_col = flt["col"]
|
||||||
|
val = flt.get("val")
|
||||||
|
flt_grain = flt.get("grain")
|
||||||
|
op = FilterOperator(flt["op"].upper())
|
||||||
|
col_obj = None
|
||||||
|
sqla_col = None
|
||||||
|
|
||||||
|
if flt_col == DTTM_ALIAS and is_timeseries and dttm_col:
|
||||||
|
col_obj = dttm_col
|
||||||
|
elif is_adhoc_column(flt_col):
|
||||||
|
try:
|
||||||
|
sqla_col = datasource.adhoc_column_to_sqla(
|
||||||
|
flt_col, force_type_check=True
|
||||||
|
)
|
||||||
|
applied_adhoc_filters_columns.append(flt_col)
|
||||||
|
except Exception: # ColumnNotFoundException
|
||||||
|
rejected_adhoc_filters_columns.append(flt_col)
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
col_obj = self.columns_by_name.get(str(flt_col))
|
||||||
|
filter_grain = flt.get("grain")
|
||||||
|
|
||||||
|
if get_column_name(flt_col) in removed_filters:
|
||||||
|
# Skip generating SQLA filter when the jinja template handles it.
|
||||||
|
continue
|
||||||
|
|
||||||
|
if col_obj or sqla_col is not None:
|
||||||
|
db_engine_spec = datasource.database.db_engine_spec
|
||||||
|
|
||||||
|
if sqla_col is not None:
|
||||||
|
pass
|
||||||
|
elif col_obj and filter_grain:
|
||||||
|
sqla_col = col_obj.get_timestamp_expression(
|
||||||
|
time_grain=filter_grain, template_processor=template_processor
|
||||||
|
)
|
||||||
|
elif col_obj:
|
||||||
|
sqla_col = datasource.convert_tbl_column_to_sqla_col(
|
||||||
|
tbl_column=col_obj, template_processor=template_processor
|
||||||
|
)
|
||||||
|
|
||||||
|
col_type = col_obj.type if col_obj else None
|
||||||
|
col_spec = db_engine_spec.get_column_spec(native_type=col_type)
|
||||||
|
is_list_target = op in (
|
||||||
|
FilterOperator.IN,
|
||||||
|
FilterOperator.NOT_IN,
|
||||||
|
)
|
||||||
|
|
||||||
|
col_advanced_data_type = col_obj.advanced_data_type if col_obj else ""
|
||||||
|
|
||||||
|
if col_spec and not col_advanced_data_type:
|
||||||
|
target_generic_type = col_spec.generic_type
|
||||||
|
else:
|
||||||
|
target_generic_type = GenericDataType.STRING
|
||||||
|
|
||||||
|
eq = datasource.filter_values_handler(
|
||||||
|
values=val,
|
||||||
|
operator=op,
|
||||||
|
target_generic_type=target_generic_type,
|
||||||
|
target_native_type=col_type,
|
||||||
|
is_list_target=is_list_target,
|
||||||
|
db_engine_spec=db_engine_spec,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get ADVANCED_DATA_TYPES from config when needed
|
||||||
|
ADVANCED_DATA_TYPES = current_app.config.get("ADVANCED_DATA_TYPES", {}) # noqa: N806
|
||||||
|
|
||||||
|
if (
|
||||||
|
col_advanced_data_type != ""
|
||||||
|
and feature_flag_manager.is_feature_enabled(
|
||||||
|
"ENABLE_ADVANCED_DATA_TYPES"
|
||||||
|
)
|
||||||
|
and col_advanced_data_type in ADVANCED_DATA_TYPES
|
||||||
|
and eq is not None
|
||||||
|
):
|
||||||
|
where_clause_and.append(
|
||||||
|
datasource._apply_advanced_data_type_filter(
|
||||||
|
sqla_col, col_advanced_data_type, op, eq
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif is_list_target:
|
||||||
|
assert isinstance(eq, (tuple, list))
|
||||||
|
if len(eq) == 0:
|
||||||
|
raise QueryObjectValidationError(
|
||||||
|
"Filter value list cannot be empty"
|
||||||
|
)
|
||||||
|
if len(eq) > len(
|
||||||
|
eq_without_none := [x for x in eq if x is not None]
|
||||||
|
):
|
||||||
|
is_null_cond = sqla_col.is_(None)
|
||||||
|
if eq:
|
||||||
|
cond = or_(is_null_cond, sqla_col.in_(eq_without_none))
|
||||||
|
else:
|
||||||
|
cond = is_null_cond
|
||||||
|
else:
|
||||||
|
cond = sqla_col.in_(eq)
|
||||||
|
if op == FilterOperator.NOT_IN:
|
||||||
|
cond = ~cond
|
||||||
|
where_clause_and.append(cond)
|
||||||
|
elif op in {
|
||||||
|
FilterOperator.IS_NULL,
|
||||||
|
FilterOperator.IS_NOT_NULL,
|
||||||
|
}:
|
||||||
|
where_clause_and.append(
|
||||||
|
db_engine_spec.handle_null_filter(sqla_col, op)
|
||||||
|
)
|
||||||
|
elif op == FilterOperator.IS_TRUE:
|
||||||
|
where_clause_and.append(
|
||||||
|
db_engine_spec.handle_boolean_filter(sqla_col, op, True)
|
||||||
|
)
|
||||||
|
elif op == FilterOperator.IS_FALSE:
|
||||||
|
where_clause_and.append(
|
||||||
|
db_engine_spec.handle_boolean_filter(sqla_col, op, False)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if (
|
||||||
|
op
|
||||||
|
not in {
|
||||||
|
FilterOperator.EQUALS,
|
||||||
|
FilterOperator.NOT_EQUALS,
|
||||||
|
}
|
||||||
|
and eq is None
|
||||||
|
):
|
||||||
|
raise QueryObjectValidationError(
|
||||||
|
"Must specify a value for filters with comparison operators"
|
||||||
|
)
|
||||||
|
if op in {
|
||||||
|
FilterOperator.EQUALS,
|
||||||
|
FilterOperator.NOT_EQUALS,
|
||||||
|
FilterOperator.GREATER_THAN,
|
||||||
|
FilterOperator.LESS_THAN,
|
||||||
|
FilterOperator.GREATER_THAN_OR_EQUALS,
|
||||||
|
FilterOperator.LESS_THAN_OR_EQUALS,
|
||||||
|
}:
|
||||||
|
where_clause_and.append(
|
||||||
|
db_engine_spec.handle_comparison_filter(sqla_col, op, eq)
|
||||||
|
)
|
||||||
|
elif op in {
|
||||||
|
FilterOperator.ILIKE,
|
||||||
|
FilterOperator.LIKE,
|
||||||
|
}:
|
||||||
|
if target_generic_type != GenericDataType.STRING:
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
sqla_col = sa.cast(sqla_col, sa.String)
|
||||||
|
|
||||||
|
if op == FilterOperator.LIKE:
|
||||||
|
where_clause_and.append(sqla_col.like(eq))
|
||||||
|
else:
|
||||||
|
where_clause_and.append(sqla_col.ilike(eq))
|
||||||
|
elif op in {FilterOperator.NOT_LIKE}:
|
||||||
|
if target_generic_type != GenericDataType.STRING:
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
sqla_col = sa.cast(sqla_col, sa.String)
|
||||||
|
|
||||||
|
where_clause_and.append(sqla_col.not_like(eq))
|
||||||
|
elif (
|
||||||
|
op == FilterOperator.TEMPORAL_RANGE
|
||||||
|
and isinstance(eq, str)
|
||||||
|
and col_obj is not None
|
||||||
|
):
|
||||||
|
_since, _until = get_since_until_from_time_range(
|
||||||
|
time_range=eq,
|
||||||
|
time_shift=self.time_shift,
|
||||||
|
extras=self.extras or {},
|
||||||
|
)
|
||||||
|
where_clause_and.append(
|
||||||
|
datasource.get_time_filter(
|
||||||
|
time_col=col_obj,
|
||||||
|
start_dttm=_since,
|
||||||
|
end_dttm=_until,
|
||||||
|
time_grain=flt_grain,
|
||||||
|
label=sqla_col.key,
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise QueryObjectValidationError(
|
||||||
|
f"Invalid filter operation type: {op}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process WHERE and HAVING extras
|
||||||
|
if self.extras:
|
||||||
|
where = self.extras.get("where")
|
||||||
|
if where:
|
||||||
|
where = datasource._process_sql_expression(
|
||||||
|
expression=where,
|
||||||
|
database_id=datasource.database_id,
|
||||||
|
engine=datasource.database.backend,
|
||||||
|
schema=datasource.schema,
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
where_clause_and += [datasource.text(where)]
|
||||||
|
having = self.extras.get("having")
|
||||||
|
if having:
|
||||||
|
having = datasource._process_sql_expression(
|
||||||
|
expression=having,
|
||||||
|
database_id=datasource.database_id,
|
||||||
|
engine=datasource.database.backend,
|
||||||
|
schema=datasource.schema,
|
||||||
|
template_processor=template_processor,
|
||||||
|
)
|
||||||
|
having_clause_and += [datasource.text(having)]
|
||||||
|
|
||||||
|
return where_clause_and, having_clause_and
|
||||||
|
|
||||||
def validate(
|
def validate(
|
||||||
self, raise_exceptions: bool | None = True
|
self, raise_exceptions: bool | None = True
|
||||||
) -> QueryObjectValidationError | None:
|
) -> QueryObjectValidationError | None:
|
||||||
@@ -350,7 +806,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
|||||||
"inner_to_dttm": self.inner_to_dttm,
|
"inner_to_dttm": self.inner_to_dttm,
|
||||||
"is_rowcount": self.is_rowcount,
|
"is_rowcount": self.is_rowcount,
|
||||||
"is_timeseries": self.is_timeseries,
|
"is_timeseries": self.is_timeseries,
|
||||||
"metrics": self.metrics,
|
"metrics": self.metrics if self.metrics else None,
|
||||||
"order_desc": self.order_desc,
|
"order_desc": self.order_desc,
|
||||||
"orderby": self.orderby,
|
"orderby": self.orderby,
|
||||||
"row_limit": self.row_limit,
|
"row_limit": self.row_limit,
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ from sqlalchemy.types import JSON
|
|||||||
from superset import db, is_feature_enabled, security_manager
|
from superset import db, is_feature_enabled, security_manager
|
||||||
from superset.commands.dataset.exceptions import DatasetNotFoundError
|
from superset.commands.dataset.exceptions import DatasetNotFoundError
|
||||||
from superset.common.db_query_status import QueryStatus
|
from superset.common.db_query_status import QueryStatus
|
||||||
|
from superset.common.query_object import QueryObject
|
||||||
from superset.connectors.sqla.utils import (
|
from superset.connectors.sqla.utils import (
|
||||||
get_columns_description,
|
get_columns_description,
|
||||||
get_physical_table_metadata,
|
get_physical_table_metadata,
|
||||||
@@ -720,7 +721,7 @@ class AnnotationDatasource(BaseDatasource):
|
|||||||
def query(self, query_obj: QueryObjectDict) -> QueryResult:
|
def query(self, query_obj: QueryObjectDict) -> QueryResult:
|
||||||
error_message = None
|
error_message = None
|
||||||
qry = db.session.query(Annotation)
|
qry = db.session.query(Annotation)
|
||||||
qry = qry.filter(Annotation.layer_id == query_obj["filter"][0]["val"])
|
qry = qry.filter(Annotation.layer_id == query_obj["filters"][0]["val"])
|
||||||
if query_obj["from_dttm"]:
|
if query_obj["from_dttm"]:
|
||||||
qry = qry.filter(Annotation.start_dttm >= query_obj["from_dttm"])
|
qry = qry.filter(Annotation.start_dttm >= query_obj["from_dttm"])
|
||||||
if query_obj["to_dttm"]:
|
if query_obj["to_dttm"]:
|
||||||
@@ -1514,18 +1515,19 @@ class SqlaTable(
|
|||||||
def _get_series_orderby(
|
def _get_series_orderby(
|
||||||
self,
|
self,
|
||||||
series_limit_metric: Metric,
|
series_limit_metric: Metric,
|
||||||
metrics_by_name: dict[str, SqlMetric],
|
query_obj: QueryObject,
|
||||||
columns_by_name: dict[str, TableColumn],
|
|
||||||
template_processor: BaseTemplateProcessor | None = None,
|
template_processor: BaseTemplateProcessor | None = None,
|
||||||
) -> Column:
|
) -> Column:
|
||||||
if utils.is_adhoc_metric(series_limit_metric):
|
if utils.is_adhoc_metric(series_limit_metric):
|
||||||
assert isinstance(series_limit_metric, dict)
|
assert isinstance(series_limit_metric, dict)
|
||||||
ob = self.adhoc_metric_to_sqla(series_limit_metric, columns_by_name)
|
ob = self.adhoc_metric_to_sqla(
|
||||||
|
series_limit_metric, query_obj.columns_by_name
|
||||||
|
)
|
||||||
elif (
|
elif (
|
||||||
isinstance(series_limit_metric, str)
|
isinstance(series_limit_metric, str)
|
||||||
and series_limit_metric in metrics_by_name
|
and series_limit_metric in query_obj.metrics_by_name
|
||||||
):
|
):
|
||||||
ob = metrics_by_name[series_limit_metric].get_sqla_col(
|
ob = query_obj.metrics_by_name[series_limit_metric].get_sqla_col(
|
||||||
template_processor=template_processor
|
template_processor=template_processor
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -1857,7 +1859,10 @@ class SqlaTable(
|
|||||||
"""
|
"""
|
||||||
extra_cache_keys = super().get_extra_cache_keys(query_obj)
|
extra_cache_keys = super().get_extra_cache_keys(query_obj)
|
||||||
if self.has_extra_cache_key_calls(query_obj):
|
if self.has_extra_cache_key_calls(query_obj):
|
||||||
sqla_query = self.get_sqla_query(**query_obj)
|
from superset.common.query_object import QueryObject
|
||||||
|
|
||||||
|
query_object = QueryObject(datasource=self, **query_obj)
|
||||||
|
sqla_query = self.get_sqla_query(query_object)
|
||||||
extra_cache_keys += sqla_query.extra_cache_keys
|
extra_cache_keys += sqla_query.extra_cache_keys
|
||||||
return list(set(extra_cache_keys))
|
return list(set(extra_cache_keys))
|
||||||
|
|
||||||
|
|||||||
@@ -940,7 +940,7 @@ def dataset_macro(
|
|||||||
metrics = [metric.metric_name for metric in dataset.metrics]
|
metrics = [metric.metric_name for metric in dataset.metrics]
|
||||||
query_obj = {
|
query_obj = {
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"metrics": metrics if include_metrics else None,
|
"metrics": metrics if include_metrics else None,
|
||||||
"columns": columns,
|
"columns": columns,
|
||||||
"from_dttm": from_dttm,
|
"from_dttm": from_dttm,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,7 @@ SQLGLOT_DIALECTS = {
|
|||||||
# "denodo": ???
|
# "denodo": ???
|
||||||
"dremio": Dremio,
|
"dremio": Dremio,
|
||||||
"drill": Dialects.DRILL,
|
"drill": Dialects.DRILL,
|
||||||
"druid": Dialects.DRUID,
|
# "druid": Dialects.DRUID, # DRUID dialect not available in current sqlglot version
|
||||||
"duckdb": Dialects.DUCKDB,
|
"duckdb": Dialects.DUCKDB,
|
||||||
# "dynamodb": ???
|
# "dynamodb": ???
|
||||||
# "elasticsearch": ???
|
# "elasticsearch": ???
|
||||||
|
|||||||
@@ -411,7 +411,7 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
|||||||
"groupby": groupby,
|
"groupby": groupby,
|
||||||
"metrics": metrics,
|
"metrics": metrics,
|
||||||
"row_limit": row_limit,
|
"row_limit": row_limit,
|
||||||
"filter": self.form_data.get("filters", []),
|
"filters": self.form_data.get("filters", []),
|
||||||
"timeseries_limit": limit,
|
"timeseries_limit": limit,
|
||||||
"extras": extras,
|
"extras": extras,
|
||||||
"timeseries_limit_metric": timeseries_limit_metric,
|
"timeseries_limit_metric": timeseries_limit_metric,
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ query_birth_names = {
|
|||||||
"row_limit": 100,
|
"row_limit": 100,
|
||||||
"granularity": "ds",
|
"granularity": "ds",
|
||||||
"time_range": "100 years ago : now",
|
"time_range": "100 years ago : now",
|
||||||
"timeseries_limit": 0,
|
"series_limit": 0,
|
||||||
"timeseries_limit_metric": None,
|
"series_limit_metric": None,
|
||||||
"order_desc": True,
|
"order_desc": True,
|
||||||
"filters": [
|
"filters": [
|
||||||
{"col": "gender", "op": "==", "val": "boy"},
|
{"col": "gender", "op": "==", "val": "boy"},
|
||||||
|
|||||||
@@ -902,6 +902,9 @@ class TestPostChartDataApi(BaseTestChartDataApi):
|
|||||||
request_payload["queries"][0]["columns"] = ["foo", "bar", "state"]
|
request_payload["queries"][0]["columns"] = ["foo", "bar", "state"]
|
||||||
request_payload["queries"][0]["where"] = "':abc' != ':xyz:qwerty'"
|
request_payload["queries"][0]["where"] = "':abc' != ':xyz:qwerty'"
|
||||||
request_payload["queries"][0]["orderby"] = None
|
request_payload["queries"][0]["orderby"] = None
|
||||||
|
request_payload["queries"][0]["granularity"] = (
|
||||||
|
None # Virtual table has no time column
|
||||||
|
)
|
||||||
request_payload["queries"][0]["metrics"] = [
|
request_payload["queries"][0]["metrics"] = [
|
||||||
{
|
{
|
||||||
"expressionType": AdhocMetricExpressionType.SQL,
|
"expressionType": AdhocMetricExpressionType.SQL,
|
||||||
@@ -1012,7 +1015,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
|||||||
"orderby": [["sum__num", False]],
|
"orderby": [["sum__num", False]],
|
||||||
"annotation_layers": [],
|
"annotation_layers": [],
|
||||||
"row_limit": 50000,
|
"row_limit": 50000,
|
||||||
"timeseries_limit": 0,
|
"series_limit": 0,
|
||||||
"order_desc": True,
|
"order_desc": True,
|
||||||
"url_params": {},
|
"url_params": {},
|
||||||
"custom_params": {},
|
"custom_params": {},
|
||||||
@@ -1065,7 +1068,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
|||||||
"orderby": [["sum__num", False]],
|
"orderby": [["sum__num", False]],
|
||||||
"annotation_layers": [],
|
"annotation_layers": [],
|
||||||
"row_limit": 50000,
|
"row_limit": 50000,
|
||||||
"timeseries_limit": 0,
|
"series_limit": 0,
|
||||||
"order_desc": True,
|
"order_desc": True,
|
||||||
"url_params": {},
|
"url_params": {},
|
||||||
"custom_params": {},
|
"custom_params": {},
|
||||||
@@ -1119,7 +1122,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
|||||||
"orderby": [["sum__num", False]],
|
"orderby": [["sum__num", False]],
|
||||||
"annotation_layers": [],
|
"annotation_layers": [],
|
||||||
"row_limit": 50000,
|
"row_limit": 50000,
|
||||||
"timeseries_limit": 0,
|
"series_limit": 0,
|
||||||
"order_desc": True,
|
"order_desc": True,
|
||||||
"url_params": {},
|
"url_params": {},
|
||||||
"custom_params": {},
|
"custom_params": {},
|
||||||
|
|||||||
@@ -124,17 +124,6 @@ class TestDatasource(SupersetTestCase):
|
|||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
|
|
||||||
query_obj = {
|
|
||||||
"columns": ["metric"],
|
|
||||||
"filter": [],
|
|
||||||
"from_dttm": datetime.now() - timedelta(days=1),
|
|
||||||
"granularity": "additional_dttm",
|
|
||||||
"orderby": [],
|
|
||||||
"to_dttm": datetime.now() + timedelta(days=1),
|
|
||||||
"series_columns": [],
|
|
||||||
"row_limit": 1000,
|
|
||||||
"row_offset": 0,
|
|
||||||
}
|
|
||||||
table = SqlaTable(
|
table = SqlaTable(
|
||||||
table_name="dummy_sql_table",
|
table_name="dummy_sql_table",
|
||||||
database=database,
|
database=database,
|
||||||
@@ -149,13 +138,28 @@ class TestDatasource(SupersetTestCase):
|
|||||||
sql=sql,
|
sql=sql,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from superset.common.query_object import QueryObject
|
||||||
|
|
||||||
|
query_obj = QueryObject(
|
||||||
|
columns=["metric"],
|
||||||
|
filters=[],
|
||||||
|
from_dttm=datetime.now() - timedelta(days=1),
|
||||||
|
granularity="additional_dttm",
|
||||||
|
orderby=[],
|
||||||
|
to_dttm=datetime.now() + timedelta(days=1),
|
||||||
|
series_columns=[],
|
||||||
|
row_limit=1000,
|
||||||
|
row_offset=0,
|
||||||
|
datasource=table,
|
||||||
|
)
|
||||||
|
|
||||||
with create_and_cleanup_table(table):
|
with create_and_cleanup_table(table):
|
||||||
table.always_filter_main_dttm = False
|
table.always_filter_main_dttm = False
|
||||||
result = str(table.get_sqla_query(**query_obj).sqla_query.whereclause)
|
result = str(table.get_sqla_query(query_obj).sqla_query.whereclause)
|
||||||
assert "default_dttm" not in result and "additional_dttm" in result # noqa: PT018
|
assert "default_dttm" not in result and "additional_dttm" in result # noqa: PT018
|
||||||
|
|
||||||
table.always_filter_main_dttm = True
|
table.always_filter_main_dttm = True
|
||||||
result = str(table.get_sqla_query(**query_obj).sqla_query.whereclause)
|
result = str(table.get_sqla_query(query_obj).sqla_query.whereclause)
|
||||||
assert "default_dttm" in result and "additional_dttm" in result # noqa: PT018
|
assert "default_dttm" in result and "additional_dttm" in result # noqa: PT018
|
||||||
|
|
||||||
def test_external_metadata_for_virtual_table(self):
|
def test_external_metadata_for_virtual_table(self):
|
||||||
@@ -584,7 +588,11 @@ def test_get_samples_with_incorrect_cc(test_client, login_as_admin, virtual_data
|
|||||||
)
|
)
|
||||||
rv = test_client.post(uri, json={})
|
rv = test_client.post(uri, json={})
|
||||||
assert rv.status_code == 422
|
assert rv.status_code == 422
|
||||||
assert rv.json["errors"][0]["error_type"] == "INVALID_SQL_ERROR"
|
# The error handling returns a simple error message for CommandInvalidError
|
||||||
|
assert "error" in rv.json
|
||||||
|
assert (
|
||||||
|
"DUMMY CC" in rv.json["error"]
|
||||||
|
) # Check the error mentions the problematic column
|
||||||
|
|
||||||
|
|
||||||
@with_feature_flags(ALLOW_ADHOC_SUBQUERY=True)
|
@with_feature_flags(ALLOW_ADHOC_SUBQUERY=True)
|
||||||
|
|||||||
@@ -186,6 +186,6 @@ def _get_energy_slices():
|
|||||||
"xscale_interval": "1",
|
"xscale_interval": "1",
|
||||||
"yscale_interval": "1",
|
"yscale_interval": "1",
|
||||||
},
|
},
|
||||||
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"timeseries_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
|
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ chart_config: dict[str, Any] = {
|
|||||||
},
|
},
|
||||||
"viz_type": "deck_path",
|
"viz_type": "deck_path",
|
||||||
},
|
},
|
||||||
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"timeseries_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
|
"query_context": '{"datasource":{"id":12,"type":"table"},"force":false,"queries":[{"time_range":" : ","filters":[],"extras":{"time_grain_sqla":null,"having":"","where":""},"applied_time_extras":{},"columns":[],"metrics":[],"annotation_layers":[],"row_limit":5000,"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"result_format":"json","result_type":"full"}', # noqa: E501
|
||||||
"cache_timeout": None,
|
"cache_timeout": None,
|
||||||
"uuid": "0c23747a-6528-4629-97bf-e4b78d3b9df1",
|
"uuid": "0c23747a-6528-4629-97bf-e4b78d3b9df1",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|||||||
@@ -294,14 +294,17 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
payload = get_query_context("birth_names")
|
payload = get_query_context("birth_names")
|
||||||
columns = payload["queries"][0]["columns"]
|
columns = payload["queries"][0]["columns"]
|
||||||
payload["queries"][0]["groupby"] = columns
|
payload["queries"][0]["groupby"] = columns
|
||||||
payload["queries"][0]["timeseries_limit"] = 99
|
payload["queries"][0]["series_limit"] = 99
|
||||||
payload["queries"][0]["timeseries_limit_metric"] = "sum__num"
|
payload["queries"][0]["series_limit_metric"] = "sum__num"
|
||||||
del payload["queries"][0]["columns"]
|
del payload["queries"][0]["columns"]
|
||||||
|
# Remove granularity so granularity_sqla can be used
|
||||||
|
del payload["queries"][0]["granularity"]
|
||||||
payload["queries"][0]["granularity_sqla"] = "timecol"
|
payload["queries"][0]["granularity_sqla"] = "timecol"
|
||||||
payload["queries"][0]["having_filters"] = [{"col": "a", "op": "==", "val": "b"}]
|
payload["queries"][0]["having_filters"] = [{"col": "a", "op": "==", "val": "b"}]
|
||||||
query_context = ChartDataQueryContextSchema().load(payload)
|
query_context = ChartDataQueryContextSchema().load(payload)
|
||||||
assert len(query_context.queries) == 1
|
assert len(query_context.queries) == 1
|
||||||
query_object = query_context.queries[0]
|
query_object = query_context.queries[0]
|
||||||
|
# granularity should be set from granularity_sqla since granularity was not set
|
||||||
assert query_object.granularity == "timecol"
|
assert query_object.granularity == "timecol"
|
||||||
assert query_object.columns == columns
|
assert query_object.columns == columns
|
||||||
assert query_object.series_limit == 99
|
assert query_object.series_limit == 99
|
||||||
@@ -520,7 +523,7 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||||
payload["queries"][0]["groupby"] = ["name"]
|
payload["queries"][0]["groupby"] = ["name"]
|
||||||
payload["queries"][0]["is_timeseries"] = True
|
payload["queries"][0]["is_timeseries"] = True
|
||||||
payload["queries"][0]["timeseries_limit"] = 5
|
payload["queries"][0]["series_limit"] = 5
|
||||||
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
|
payload["queries"][0]["time_offsets"] = ["1 year ago", "1 year later"]
|
||||||
payload["queries"][0]["time_range"] = "1990 : 1991"
|
payload["queries"][0]["time_range"] = "1990 : 1991"
|
||||||
query_context = ChartDataQueryContextSchema().load(payload)
|
query_context = ChartDataQueryContextSchema().load(payload)
|
||||||
@@ -556,7 +559,7 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
# due to "name" is random generated, each time_offset slice will be empty
|
# due to "name" is random generated, each time_offset slice will be empty
|
||||||
payload["queries"][0]["groupby"] = ["name"]
|
payload["queries"][0]["groupby"] = ["name"]
|
||||||
payload["queries"][0]["is_timeseries"] = True
|
payload["queries"][0]["is_timeseries"] = True
|
||||||
payload["queries"][0]["timeseries_limit"] = 5
|
payload["queries"][0]["series_limit"] = 5
|
||||||
payload["queries"][0]["time_offsets"] = []
|
payload["queries"][0]["time_offsets"] = []
|
||||||
payload["queries"][0]["time_range"] = "1990 : 1991"
|
payload["queries"][0]["time_range"] = "1990 : 1991"
|
||||||
payload["queries"][0]["granularity"] = "ds"
|
payload["queries"][0]["granularity"] = "ds"
|
||||||
@@ -609,7 +612,7 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||||
payload["queries"][0]["groupby"] = ["state"]
|
payload["queries"][0]["groupby"] = ["state"]
|
||||||
payload["queries"][0]["is_timeseries"] = True
|
payload["queries"][0]["is_timeseries"] = True
|
||||||
payload["queries"][0]["timeseries_limit"] = 5
|
payload["queries"][0]["series_limit"] = 5
|
||||||
payload["queries"][0]["time_offsets"] = []
|
payload["queries"][0]["time_offsets"] = []
|
||||||
payload["queries"][0]["time_range"] = "1980 : 1991"
|
payload["queries"][0]["time_range"] = "1980 : 1991"
|
||||||
payload["queries"][0]["granularity"] = "ds"
|
payload["queries"][0]["granularity"] = "ds"
|
||||||
@@ -638,9 +641,11 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
def test_time_offsets_accuracy(self):
|
def test_time_offsets_accuracy(self):
|
||||||
payload = get_query_context("birth_names")
|
payload = get_query_context("birth_names")
|
||||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||||
payload["queries"][0]["groupby"] = ["state"]
|
payload["queries"][0]["columns"] = [
|
||||||
|
"state"
|
||||||
|
] # Use columns instead of deprecated groupby
|
||||||
payload["queries"][0]["is_timeseries"] = True
|
payload["queries"][0]["is_timeseries"] = True
|
||||||
payload["queries"][0]["timeseries_limit"] = 5
|
payload["queries"][0]["series_limit"] = 5
|
||||||
payload["queries"][0]["time_offsets"] = []
|
payload["queries"][0]["time_offsets"] = []
|
||||||
payload["queries"][0]["time_range"] = "1980 : 1991"
|
payload["queries"][0]["time_range"] = "1980 : 1991"
|
||||||
payload["queries"][0]["granularity"] = "ds"
|
payload["queries"][0]["granularity"] = "ds"
|
||||||
@@ -713,10 +718,10 @@ class TestQueryContext(SupersetTestCase):
|
|||||||
"sqlExpression": "ds",
|
"sqlExpression": "ds",
|
||||||
"label": "ds",
|
"label": "ds",
|
||||||
"expressionType": "SQL",
|
"expressionType": "SQL",
|
||||||
}
|
},
|
||||||
|
"name", # Add name to columns instead of using deprecated groupby
|
||||||
]
|
]
|
||||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||||
payload["queries"][0]["groupby"] = ["name"]
|
|
||||||
payload["queries"][0]["is_timeseries"] = True
|
payload["queries"][0]["is_timeseries"] = True
|
||||||
payload["queries"][0]["row_limit"] = 100
|
payload["queries"][0]["row_limit"] = 100
|
||||||
payload["queries"][0]["row_offset"] = 10
|
payload["queries"][0]["row_offset"] = 10
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ from sqlalchemy.sql import text
|
|||||||
from sqlalchemy.sql.elements import TextClause
|
from sqlalchemy.sql.elements import TextClause
|
||||||
|
|
||||||
from superset import db
|
from superset import db
|
||||||
|
from superset.common.query_object import QueryObject
|
||||||
from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric
|
from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric
|
||||||
from superset.constants import EMPTY_STRING, NULL_STRING
|
from superset.constants import EMPTY_STRING, NULL_STRING
|
||||||
from superset.db_engine_specs.bigquery import BigQueryEngineSpec
|
from superset.db_engine_specs.bigquery import BigQueryEngineSpec
|
||||||
@@ -162,7 +163,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"count_timegrain",
|
"count_timegrain",
|
||||||
],
|
],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"extras": {"time_grain_sqla": "P1D"},
|
"extras": {"time_grain_sqla": "P1D"},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +187,8 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
sqla_query = table.get_sqla_query(**base_query_obj)
|
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||||
|
sqla_query = table.get_sqla_query(query_object)
|
||||||
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||||
|
|
||||||
# assert virtual dataset
|
# assert virtual dataset
|
||||||
@@ -234,12 +236,13 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"extras": {"time_grain_sqla": "P1D"},
|
"extras": {"time_grain_sqla": "P1D"},
|
||||||
}
|
}
|
||||||
mock_dataset_id_from_context.return_value = table.id
|
mock_dataset_id_from_context.return_value = table.id
|
||||||
|
|
||||||
sqla_query = table.get_sqla_query(**base_query_obj)
|
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||||
|
sqla_query = table.get_sqla_query(query_object)
|
||||||
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
query = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||||
|
|
||||||
database = table.database
|
database = table.database
|
||||||
@@ -267,7 +270,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
table = SqlaTable(
|
table = SqlaTable(
|
||||||
@@ -275,8 +278,9 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||||
with pytest.raises(QueryObjectValidationError):
|
with pytest.raises(QueryObjectValidationError):
|
||||||
table.get_sqla_query(**base_query_obj)
|
table.get_sqla_query(query_object)
|
||||||
# Cleanup
|
# Cleanup
|
||||||
db.session.delete(table)
|
db.session.delete(table)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
@@ -310,7 +314,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["gender"],
|
"groupby": ["gender"],
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": filter_.column,
|
"col": filter_.column,
|
||||||
"op": filter_.operator,
|
"op": filter_.operator,
|
||||||
@@ -319,7 +323,8 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
],
|
],
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
sqla_query = table.get_sqla_query(**query_obj)
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
|
sqla_query = table.get_sqla_query(query_object)
|
||||||
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
|
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||||
if isinstance(filter_.expected, list):
|
if isinstance(filter_.expected, list):
|
||||||
assert any([candidate in sql for candidate in filter_.expected]) # noqa: C419
|
assert any([candidate in sql for candidate in filter_.expected]) # noqa: C419
|
||||||
@@ -344,7 +349,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["boolean_gender"],
|
"groupby": ["boolean_gender"],
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "boolean_gender",
|
"col": "boolean_gender",
|
||||||
"op": FilterOperator.IN,
|
"op": FilterOperator.IN,
|
||||||
@@ -353,7 +358,8 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
],
|
],
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
sqla_query = table.get_sqla_query(**query_obj)
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
|
sqla_query = table.get_sqla_query(query_object)
|
||||||
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
|
sql = table.database.compile_sqla_query(sqla_query.sqla_query)
|
||||||
dialect = table.database.get_dialect()
|
dialect = table.database.get_dialect()
|
||||||
operand = "(true, false)"
|
operand = "(true, false)"
|
||||||
@@ -371,7 +377,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["user"],
|
"groupby": ["user"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +389,9 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
# TODO(villebro): make it work with presto
|
# TODO(villebro): make it work with presto
|
||||||
if get_example_database().backend != "presto":
|
if get_example_database().backend != "presto":
|
||||||
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
with pytest.raises(QueryObjectValidationError):
|
with pytest.raises(QueryObjectValidationError):
|
||||||
table.get_sqla_query(**query_obj)
|
table.get_sqla_query(query_object)
|
||||||
|
|
||||||
def test_query_format_strip_trailing_semicolon(self):
|
def test_query_format_strip_trailing_semicolon(self):
|
||||||
query_obj = {
|
query_obj = {
|
||||||
@@ -394,7 +401,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["user"],
|
"groupby": ["user"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +410,8 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
sql="SELECT * from test_table;",
|
sql="SELECT * from test_table;",
|
||||||
database=get_example_database(),
|
database=get_example_database(),
|
||||||
)
|
)
|
||||||
sqlaq = table.get_sqla_query(**query_obj)
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
|
sqlaq = table.get_sqla_query(query_object)
|
||||||
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
||||||
assert sql[-1] != ";"
|
assert sql[-1] != ";"
|
||||||
|
|
||||||
@@ -415,7 +423,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["grp"],
|
"groupby": ["grp"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
table = SqlaTable(
|
table = SqlaTable(
|
||||||
@@ -425,8 +433,9 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
query_obj = dict(**base_query_obj, extras={})
|
query_obj = dict(**base_query_obj, extras={})
|
||||||
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
with pytest.raises(QueryObjectValidationError):
|
with pytest.raises(QueryObjectValidationError):
|
||||||
table.get_sqla_query(**query_obj)
|
table.get_sqla_query(query_object)
|
||||||
|
|
||||||
def test_dml_statement_raises_exception(self):
|
def test_dml_statement_raises_exception(self):
|
||||||
base_query_obj = {
|
base_query_obj = {
|
||||||
@@ -436,7 +445,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
"groupby": ["grp"],
|
"groupby": ["grp"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
table = SqlaTable(
|
table = SqlaTable(
|
||||||
@@ -446,8 +455,9 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
query_obj = dict(**base_query_obj, extras={})
|
query_obj = dict(**base_query_obj, extras={})
|
||||||
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
with pytest.raises(QueryObjectValidationError):
|
with pytest.raises(QueryObjectValidationError):
|
||||||
table.get_sqla_query(**query_obj)
|
table.get_sqla_query(query_object)
|
||||||
|
|
||||||
def test_fetch_metadata_for_updated_virtual_table(self):
|
def test_fetch_metadata_for_updated_virtual_table(self):
|
||||||
table = SqlaTable(
|
table = SqlaTable(
|
||||||
@@ -507,7 +517,7 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"extras": {},
|
"extras": {},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -516,7 +526,8 @@ class TestDatabaseModel(SupersetTestCase):
|
|||||||
db.session.add(database)
|
db.session.add(database)
|
||||||
db.session.add(table)
|
db.session.add(table)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
sqlaq = table.get_sqla_query(**query_obj)
|
query_object = QueryObject(datasource=table, **query_obj)
|
||||||
|
sqlaq = table.get_sqla_query(query_object)
|
||||||
assert sqlaq.labels_expected == ["user", "COUNT_DISTINCT(user)"]
|
assert sqlaq.labels_expected == ["user", "COUNT_DISTINCT(user)"]
|
||||||
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
||||||
assert "COUNT_DISTINCT_user__00db1" in sql
|
assert "COUNT_DISTINCT_user__00db1" in sql
|
||||||
@@ -585,7 +596,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
"filters": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -595,7 +606,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "foo", "val": [None], "op": "IN"}],
|
"filters": [{"col": "foo", "val": [None], "op": "IN"}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -605,7 +616,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
"filters": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -615,7 +626,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "foo", "val": [""], "op": "IN"}],
|
"filters": [{"col": "foo", "val": [""], "op": "IN"}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -625,7 +636,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "foo",
|
"col": "foo",
|
||||||
"val": [EMPTY_STRING, NULL_STRING, "null", "foo"],
|
"val": [EMPTY_STRING, NULL_STRING, "null", "foo"],
|
||||||
@@ -641,7 +652,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "foo",
|
"col": "foo",
|
||||||
"val": ['"text in double quotes"'],
|
"val": ['"text in double quotes"'],
|
||||||
@@ -657,7 +668,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "foo",
|
"col": "foo",
|
||||||
"val": ["'text in single quotes'"],
|
"val": ["'text in single quotes'"],
|
||||||
@@ -673,7 +684,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "foo",
|
"col": "foo",
|
||||||
"val": ['double quotes " in text'],
|
"val": ['double quotes " in text'],
|
||||||
@@ -689,7 +700,7 @@ def test_filter_on_text_column(text_column_table):
|
|||||||
result_object = table.query(
|
result_object = table.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "foo",
|
"col": "foo",
|
||||||
"val": ["single quotes ' in text"],
|
"val": ["single quotes ' in text"],
|
||||||
@@ -726,7 +737,7 @@ def test_should_generate_closed_and_open_time_filter_range(login_as_admin):
|
|||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
"from_dttm": datetime(2022, 1, 1),
|
"from_dttm": datetime(2022, 1, 1),
|
||||||
"to_dttm": datetime(2023, 1, 1),
|
"to_dttm": datetime(2023, 1, 1),
|
||||||
"granularity": "datetime_col",
|
"granularity": "datetime_col",
|
||||||
@@ -763,7 +774,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
|||||||
result = physical_dataset.query(
|
result = physical_dataset.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
"filters": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -782,7 +793,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
|||||||
physical_dataset.query(
|
physical_dataset.query(
|
||||||
{
|
{
|
||||||
"metrics": ["count"],
|
"metrics": ["count"],
|
||||||
"filter": [{"col": "col4", "val": None, "op": flt.value}],
|
"filters": [{"col": "col4", "val": None, "op": flt.value}],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -871,7 +882,7 @@ def test_extra_cache_keys(
|
|||||||
"groupby": ["id", "username", "email"],
|
"groupby": ["id", "username", "email"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
query_obj = dict(**base_query_obj, extras={})
|
query_obj = dict(**base_query_obj, extras={})
|
||||||
@@ -917,7 +928,7 @@ def test_extra_cache_keys_in_sql_expression(
|
|||||||
"groupby": ["id", "username", "email"],
|
"groupby": ["id", "username", "email"],
|
||||||
"metrics": [],
|
"metrics": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
query_obj = dict(**base_query_obj, extras={"where": sql_expression})
|
query_obj = dict(**base_query_obj, extras={"where": sql_expression})
|
||||||
@@ -960,7 +971,7 @@ def test_extra_cache_keys_in_adhoc_metrics_and_columns(
|
|||||||
"metrics": [],
|
"metrics": [],
|
||||||
"columns": [],
|
"columns": [],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
items: dict[str, Any] = {
|
items: dict[str, Any] = {
|
||||||
@@ -1014,7 +1025,7 @@ def test_extra_cache_keys_in_dataset_metrics_and_columns(
|
|||||||
"columns": ["username"],
|
"columns": ["username"],
|
||||||
"metrics": ["variable_profit"],
|
"metrics": ["variable_profit"],
|
||||||
"is_timeseries": False,
|
"is_timeseries": False,
|
||||||
"filter": [],
|
"filters": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
extra_cache_keys = table.get_extra_cache_keys(query_obj)
|
extra_cache_keys = table.get_extra_cache_keys(query_obj)
|
||||||
@@ -1118,7 +1129,7 @@ def test__temporal_range_operator_in_adhoc_filter(physical_dataset):
|
|||||||
result = physical_dataset.query(
|
result = physical_dataset.query(
|
||||||
{
|
{
|
||||||
"columns": ["col1", "col2"],
|
"columns": ["col1", "col2"],
|
||||||
"filter": [
|
"filters": [
|
||||||
{
|
{
|
||||||
"col": "col5",
|
"col": "col5",
|
||||||
"val": "2000-01-05 : 2000-01-06",
|
"val": "2000-01-05 : 2000-01-06",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
115
tests/unit_tests/queries/test_query_object_prequery.py
Normal file
115
tests/unit_tests/queries/test_query_object_prequery.py
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# 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 datetime import datetime
|
||||||
|
|
||||||
|
from superset.common.query_object import QueryObject
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_series_limit_prequery_obj():
|
||||||
|
"""
|
||||||
|
Test get_series_limit_prequery_obj method
|
||||||
|
"""
|
||||||
|
# Create a QueryObject with series limit settings
|
||||||
|
query_object = QueryObject(
|
||||||
|
columns=["country", "year"],
|
||||||
|
metrics=["sum__sales"],
|
||||||
|
series_limit=10,
|
||||||
|
from_dttm=datetime(2020, 1, 1),
|
||||||
|
to_dttm=datetime(2021, 1, 1),
|
||||||
|
filters=[{"col": "region", "op": "IN", "val": ["US", "EU"]}],
|
||||||
|
extras={"time_grain_sqla": "P1D"},
|
||||||
|
order_desc=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test basic prequery object creation
|
||||||
|
prequery_obj = query_object.get_series_limit_prequery_obj(
|
||||||
|
granularity="ds",
|
||||||
|
inner_from_dttm=None,
|
||||||
|
inner_to_dttm=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert prequery_obj["is_timeseries"] is False
|
||||||
|
assert prequery_obj["row_limit"] == 10
|
||||||
|
assert prequery_obj["metrics"] == ["sum__sales"]
|
||||||
|
assert prequery_obj["granularity"] == "ds"
|
||||||
|
assert prequery_obj["groupby"] == ["country", "year"]
|
||||||
|
assert prequery_obj["from_dttm"] == datetime(2020, 1, 1)
|
||||||
|
assert prequery_obj["to_dttm"] == datetime(2021, 1, 1)
|
||||||
|
assert prequery_obj["filter"] == [
|
||||||
|
{"col": "region", "op": "IN", "val": ["US", "EU"]}
|
||||||
|
]
|
||||||
|
assert prequery_obj["orderby"] == []
|
||||||
|
assert prequery_obj["extras"] == {"time_grain_sqla": "P1D"}
|
||||||
|
assert prequery_obj["order_desc"] is True # Always True for prequery
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_series_limit_prequery_obj_with_overrides():
|
||||||
|
"""
|
||||||
|
Test get_series_limit_prequery_obj with inner dates and orderby override
|
||||||
|
"""
|
||||||
|
query_object = QueryObject(
|
||||||
|
columns=["country"],
|
||||||
|
metrics=["count"],
|
||||||
|
series_limit=5,
|
||||||
|
from_dttm=datetime(2020, 1, 1),
|
||||||
|
to_dttm=datetime(2021, 1, 1),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with inner dates and custom orderby
|
||||||
|
inner_from = datetime(2020, 6, 1)
|
||||||
|
inner_to = datetime(2020, 12, 31)
|
||||||
|
custom_orderby = [("sum__revenue", False)]
|
||||||
|
|
||||||
|
prequery_obj = query_object.get_series_limit_prequery_obj(
|
||||||
|
granularity="date_col",
|
||||||
|
inner_from_dttm=inner_from,
|
||||||
|
inner_to_dttm=inner_to,
|
||||||
|
orderby=custom_orderby,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert prequery_obj["from_dttm"] == inner_from
|
||||||
|
assert prequery_obj["to_dttm"] == inner_to
|
||||||
|
assert prequery_obj["orderby"] == custom_orderby
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_series_limit_prequery_obj_base_axis_filtering():
|
||||||
|
"""
|
||||||
|
Test that base axis columns are filtered out in prequery
|
||||||
|
"""
|
||||||
|
# Mock the x-axis column with proper structure for base axis
|
||||||
|
query_object = QueryObject(
|
||||||
|
columns=[
|
||||||
|
{
|
||||||
|
"label": "__timestamp",
|
||||||
|
"sqlExpression": "__timestamp",
|
||||||
|
"columnType": "BASE_AXIS",
|
||||||
|
},
|
||||||
|
"country",
|
||||||
|
"city",
|
||||||
|
],
|
||||||
|
metrics=["revenue"],
|
||||||
|
series_limit=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
prequery_obj = query_object.get_series_limit_prequery_obj(
|
||||||
|
granularity=None,
|
||||||
|
inner_from_dttm=None,
|
||||||
|
inner_to_dttm=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
# The columns in prequery should exclude the base axis column
|
||||||
|
assert prequery_obj["columns"] == ["country", "city"]
|
||||||
Reference in New Issue
Block a user