mirror of
https://github.com/apache/superset.git
synced 2026-04-28 12:34:23 +00:00
Compare commits
21 Commits
embedded-e
...
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.is_timeseries = False
|
||||
query_obj.orderby = []
|
||||
query_obj.metrics = None
|
||||
query_obj.metrics = []
|
||||
query_obj.post_processing = []
|
||||
qry_obj_cols = []
|
||||
for o in datasource.columns:
|
||||
@@ -168,7 +168,7 @@ def _get_drill_detail(
|
||||
query_obj = copy.copy(query_obj)
|
||||
query_obj.is_timeseries = False
|
||||
query_obj.orderby = []
|
||||
query_obj.metrics = None
|
||||
query_obj.metrics = []
|
||||
query_obj.post_processing = []
|
||||
qry_obj_cols = []
|
||||
for o in datasource.columns:
|
||||
|
||||
@@ -86,6 +86,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
apply_fetch_values_predicate: bool
|
||||
columns: list[Column]
|
||||
datasource: BaseDatasource | None
|
||||
columns_by_name: dict[str, Any]
|
||||
metrics_by_name: dict[str, Any]
|
||||
extras: dict[str, Any]
|
||||
filter: list[QueryObjectFilterClause]
|
||||
from_dttm: datetime | None
|
||||
@@ -94,7 +96,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
inner_to_dttm: datetime | None
|
||||
is_rowcount: bool
|
||||
is_timeseries: bool
|
||||
metrics: list[Metric] | None
|
||||
metrics: list[Metric]
|
||||
order_desc: bool
|
||||
orderby: list[OrderBy]
|
||||
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.columns = columns or []
|
||||
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.filter = filters or []
|
||||
self.granularity = granularity
|
||||
@@ -192,9 +218,12 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
def is_str_or_adhoc(metric: Metric) -> bool:
|
||||
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
|
||||
for x in metrics
|
||||
for x in (metrics or [])
|
||||
]
|
||||
|
||||
def _set_post_processing(
|
||||
@@ -226,15 +255,20 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
field.new_name,
|
||||
)
|
||||
value = kwargs[field.old_name]
|
||||
if value:
|
||||
if hasattr(self, field.new_name):
|
||||
if value is not None:
|
||||
# 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(
|
||||
"The field `%s` is already populated, "
|
||||
"replacing value with contents from `%s`.",
|
||||
"not replacing with contents from deprecated `%s`.",
|
||||
field.new_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:
|
||||
# move deprecated extras fields to extras
|
||||
@@ -247,8 +281,8 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
field.new_name,
|
||||
)
|
||||
value = kwargs[field.old_name]
|
||||
if value:
|
||||
if hasattr(self.extras, field.new_name):
|
||||
if value is not None and value != "": # Don't add empty string values
|
||||
if field.new_name in self.extras:
|
||||
logger.warning(
|
||||
"The field `%s` is already populated in "
|
||||
"`extras`, replacing value with contents "
|
||||
@@ -262,7 +296,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
def metric_names(self) -> list[str]:
|
||||
"""Return metrics names (labels), coerce adhoc metrics to strings."""
|
||||
return get_metric_names(
|
||||
self.metrics or [],
|
||||
self.metrics,
|
||||
(
|
||||
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."""
|
||||
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(
|
||||
self, raise_exceptions: bool | None = True
|
||||
) -> QueryObjectValidationError | None:
|
||||
@@ -350,7 +806,7 @@ class QueryObject: # pylint: disable=too-many-instance-attributes
|
||||
"inner_to_dttm": self.inner_to_dttm,
|
||||
"is_rowcount": self.is_rowcount,
|
||||
"is_timeseries": self.is_timeseries,
|
||||
"metrics": self.metrics,
|
||||
"metrics": self.metrics if self.metrics else None,
|
||||
"order_desc": self.order_desc,
|
||||
"orderby": self.orderby,
|
||||
"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.commands.dataset.exceptions import DatasetNotFoundError
|
||||
from superset.common.db_query_status import QueryStatus
|
||||
from superset.common.query_object import QueryObject
|
||||
from superset.connectors.sqla.utils import (
|
||||
get_columns_description,
|
||||
get_physical_table_metadata,
|
||||
@@ -720,7 +721,7 @@ class AnnotationDatasource(BaseDatasource):
|
||||
def query(self, query_obj: QueryObjectDict) -> QueryResult:
|
||||
error_message = None
|
||||
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"]:
|
||||
qry = qry.filter(Annotation.start_dttm >= query_obj["from_dttm"])
|
||||
if query_obj["to_dttm"]:
|
||||
@@ -1514,18 +1515,19 @@ class SqlaTable(
|
||||
def _get_series_orderby(
|
||||
self,
|
||||
series_limit_metric: Metric,
|
||||
metrics_by_name: dict[str, SqlMetric],
|
||||
columns_by_name: dict[str, TableColumn],
|
||||
query_obj: QueryObject,
|
||||
template_processor: BaseTemplateProcessor | None = None,
|
||||
) -> Column:
|
||||
if utils.is_adhoc_metric(series_limit_metric):
|
||||
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 (
|
||||
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
|
||||
)
|
||||
else:
|
||||
@@ -1857,7 +1859,10 @@ class SqlaTable(
|
||||
"""
|
||||
extra_cache_keys = super().get_extra_cache_keys(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
|
||||
return list(set(extra_cache_keys))
|
||||
|
||||
|
||||
@@ -940,7 +940,7 @@ def dataset_macro(
|
||||
metrics = [metric.metric_name for metric in dataset.metrics]
|
||||
query_obj = {
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"metrics": metrics if include_metrics else None,
|
||||
"columns": columns,
|
||||
"from_dttm": from_dttm,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -70,7 +70,7 @@ SQLGLOT_DIALECTS = {
|
||||
# "denodo": ???
|
||||
"dremio": Dremio,
|
||||
"drill": Dialects.DRILL,
|
||||
"druid": Dialects.DRUID,
|
||||
# "druid": Dialects.DRUID, # DRUID dialect not available in current sqlglot version
|
||||
"duckdb": Dialects.DUCKDB,
|
||||
# "dynamodb": ???
|
||||
# "elasticsearch": ???
|
||||
|
||||
@@ -411,7 +411,7 @@ class BaseViz: # pylint: disable=too-many-public-methods
|
||||
"groupby": groupby,
|
||||
"metrics": metrics,
|
||||
"row_limit": row_limit,
|
||||
"filter": self.form_data.get("filters", []),
|
||||
"filters": self.form_data.get("filters", []),
|
||||
"timeseries_limit": limit,
|
||||
"extras": extras,
|
||||
"timeseries_limit_metric": timeseries_limit_metric,
|
||||
|
||||
@@ -29,8 +29,8 @@ query_birth_names = {
|
||||
"row_limit": 100,
|
||||
"granularity": "ds",
|
||||
"time_range": "100 years ago : now",
|
||||
"timeseries_limit": 0,
|
||||
"timeseries_limit_metric": None,
|
||||
"series_limit": 0,
|
||||
"series_limit_metric": None,
|
||||
"order_desc": True,
|
||||
"filters": [
|
||||
{"col": "gender", "op": "==", "val": "boy"},
|
||||
|
||||
@@ -902,6 +902,9 @@ class TestPostChartDataApi(BaseTestChartDataApi):
|
||||
request_payload["queries"][0]["columns"] = ["foo", "bar", "state"]
|
||||
request_payload["queries"][0]["where"] = "':abc' != ':xyz:qwerty'"
|
||||
request_payload["queries"][0]["orderby"] = None
|
||||
request_payload["queries"][0]["granularity"] = (
|
||||
None # Virtual table has no time column
|
||||
)
|
||||
request_payload["queries"][0]["metrics"] = [
|
||||
{
|
||||
"expressionType": AdhocMetricExpressionType.SQL,
|
||||
@@ -1012,7 +1015,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
@@ -1065,7 +1068,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
@@ -1119,7 +1122,7 @@ class TestGetChartDataApi(BaseTestChartDataApi):
|
||||
"orderby": [["sum__num", False]],
|
||||
"annotation_layers": [],
|
||||
"row_limit": 50000,
|
||||
"timeseries_limit": 0,
|
||||
"series_limit": 0,
|
||||
"order_desc": True,
|
||||
"url_params": {},
|
||||
"custom_params": {},
|
||||
|
||||
@@ -124,17 +124,6 @@ class TestDatasource(SupersetTestCase):
|
||||
else:
|
||||
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_name="dummy_sql_table",
|
||||
database=database,
|
||||
@@ -149,13 +138,28 @@ class TestDatasource(SupersetTestCase):
|
||||
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):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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={})
|
||||
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)
|
||||
|
||||
@@ -186,6 +186,6 @@ def _get_energy_slices():
|
||||
"xscale_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",
|
||||
},
|
||||
"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,
|
||||
"uuid": "0c23747a-6528-4629-97bf-e4b78d3b9df1",
|
||||
"version": "1.0.0",
|
||||
|
||||
@@ -294,14 +294,17 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload = get_query_context("birth_names")
|
||||
columns = payload["queries"][0]["columns"]
|
||||
payload["queries"][0]["groupby"] = columns
|
||||
payload["queries"][0]["timeseries_limit"] = 99
|
||||
payload["queries"][0]["timeseries_limit_metric"] = "sum__num"
|
||||
payload["queries"][0]["series_limit"] = 99
|
||||
payload["queries"][0]["series_limit_metric"] = "sum__num"
|
||||
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]["having_filters"] = [{"col": "a", "op": "==", "val": "b"}]
|
||||
query_context = ChartDataQueryContextSchema().load(payload)
|
||||
assert len(query_context.queries) == 1
|
||||
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.columns == columns
|
||||
assert query_object.series_limit == 99
|
||||
@@ -520,7 +523,7 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["name"]
|
||||
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_range"] = "1990 : 1991"
|
||||
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
|
||||
payload["queries"][0]["groupby"] = ["name"]
|
||||
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_range"] = "1990 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -609,7 +612,7 @@ class TestQueryContext(SupersetTestCase):
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["state"]
|
||||
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_range"] = "1980 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -638,9 +641,11 @@ class TestQueryContext(SupersetTestCase):
|
||||
def test_time_offsets_accuracy(self):
|
||||
payload = get_query_context("birth_names")
|
||||
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]["timeseries_limit"] = 5
|
||||
payload["queries"][0]["series_limit"] = 5
|
||||
payload["queries"][0]["time_offsets"] = []
|
||||
payload["queries"][0]["time_range"] = "1980 : 1991"
|
||||
payload["queries"][0]["granularity"] = "ds"
|
||||
@@ -713,10 +718,10 @@ class TestQueryContext(SupersetTestCase):
|
||||
"sqlExpression": "ds",
|
||||
"label": "ds",
|
||||
"expressionType": "SQL",
|
||||
}
|
||||
},
|
||||
"name", # Add name to columns instead of using deprecated groupby
|
||||
]
|
||||
payload["queries"][0]["metrics"] = ["sum__num"]
|
||||
payload["queries"][0]["groupby"] = ["name"]
|
||||
payload["queries"][0]["is_timeseries"] = True
|
||||
payload["queries"][0]["row_limit"] = 100
|
||||
payload["queries"][0]["row_offset"] = 10
|
||||
|
||||
@@ -33,6 +33,7 @@ from sqlalchemy.sql import text
|
||||
from sqlalchemy.sql.elements import TextClause
|
||||
|
||||
from superset import db
|
||||
from superset.common.query_object import QueryObject
|
||||
from superset.connectors.sqla.models import SqlaTable, TableColumn, SqlMetric
|
||||
from superset.constants import EMPTY_STRING, NULL_STRING
|
||||
from superset.db_engine_specs.bigquery import BigQueryEngineSpec
|
||||
@@ -162,7 +163,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"count_timegrain",
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {"time_grain_sqla": "P1D"},
|
||||
}
|
||||
|
||||
@@ -186,7 +187,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
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)
|
||||
|
||||
# assert virtual dataset
|
||||
@@ -234,12 +236,13 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
},
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {"time_grain_sqla": "P1D"},
|
||||
}
|
||||
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)
|
||||
|
||||
database = table.database
|
||||
@@ -267,7 +270,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
}
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -275,8 +278,9 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
db.session.commit()
|
||||
|
||||
query_object = QueryObject(datasource=table, **base_query_obj)
|
||||
with pytest.raises(QueryObjectValidationError):
|
||||
table.get_sqla_query(**base_query_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
# Cleanup
|
||||
db.session.delete(table)
|
||||
db.session.commit()
|
||||
@@ -310,7 +314,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["gender"],
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": filter_.column,
|
||||
"op": filter_.operator,
|
||||
@@ -319,7 +323,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
],
|
||||
"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)
|
||||
if isinstance(filter_.expected, list):
|
||||
assert any([candidate in sql for candidate in filter_.expected]) # noqa: C419
|
||||
@@ -344,7 +349,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["boolean_gender"],
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "boolean_gender",
|
||||
"op": FilterOperator.IN,
|
||||
@@ -353,7 +358,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
],
|
||||
"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)
|
||||
dialect = table.database.get_dialect()
|
||||
operand = "(true, false)"
|
||||
@@ -371,7 +377,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["user"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -383,8 +389,9 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
# TODO(villebro): make it work with presto
|
||||
if get_example_database().backend != "presto":
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
with pytest.raises(QueryObjectValidationError):
|
||||
table.get_sqla_query(**query_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
|
||||
def test_query_format_strip_trailing_semicolon(self):
|
||||
query_obj = {
|
||||
@@ -394,7 +401,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["user"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -403,7 +410,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
sql="SELECT * from test_table;",
|
||||
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)
|
||||
assert sql[-1] != ";"
|
||||
|
||||
@@ -415,7 +423,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["grp"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -425,8 +433,9 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
|
||||
query_obj = dict(**base_query_obj, extras={})
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
with pytest.raises(QueryObjectValidationError):
|
||||
table.get_sqla_query(**query_obj)
|
||||
table.get_sqla_query(query_object)
|
||||
|
||||
def test_dml_statement_raises_exception(self):
|
||||
base_query_obj = {
|
||||
@@ -436,7 +445,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
"groupby": ["grp"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
table = SqlaTable(
|
||||
@@ -446,8 +455,9 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
)
|
||||
|
||||
query_obj = dict(**base_query_obj, extras={})
|
||||
query_object = QueryObject(datasource=table, **query_obj)
|
||||
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):
|
||||
table = SqlaTable(
|
||||
@@ -507,7 +517,7 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
}
|
||||
],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"extras": {},
|
||||
}
|
||||
|
||||
@@ -516,7 +526,8 @@ class TestDatabaseModel(SupersetTestCase):
|
||||
db.session.add(database)
|
||||
db.session.add(table)
|
||||
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)"]
|
||||
sql = table.database.compile_sqla_query(sqlaq.sqla_query)
|
||||
assert "COUNT_DISTINCT_user__00db1" in sql
|
||||
@@ -585,7 +596,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [NULL_STRING], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -595,7 +606,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [None], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [None], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -605,7 +616,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [EMPTY_STRING], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -615,7 +626,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "foo", "val": [""], "op": "IN"}],
|
||||
"filters": [{"col": "foo", "val": [""], "op": "IN"}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -625,7 +636,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "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(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ['"text in double quotes"'],
|
||||
@@ -657,7 +668,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ["'text in single quotes'"],
|
||||
@@ -673,7 +684,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ['double quotes " in text'],
|
||||
@@ -689,7 +700,7 @@ def test_filter_on_text_column(text_column_table):
|
||||
result_object = table.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "foo",
|
||||
"val": ["single quotes ' in text"],
|
||||
@@ -726,7 +737,7 @@ def test_should_generate_closed_and_open_time_filter_range(login_as_admin):
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
"from_dttm": datetime(2022, 1, 1),
|
||||
"to_dttm": datetime(2023, 1, 1),
|
||||
"granularity": "datetime_col",
|
||||
@@ -763,7 +774,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
||||
result = physical_dataset.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
||||
"filters": [{"col": "col4", "val": None, "op": expected["operator"]}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -782,7 +793,7 @@ def test_none_operand_in_filter(login_as_admin, physical_dataset):
|
||||
physical_dataset.query(
|
||||
{
|
||||
"metrics": ["count"],
|
||||
"filter": [{"col": "col4", "val": None, "op": flt.value}],
|
||||
"filters": [{"col": "col4", "val": None, "op": flt.value}],
|
||||
"is_timeseries": False,
|
||||
}
|
||||
)
|
||||
@@ -871,7 +882,7 @@ def test_extra_cache_keys(
|
||||
"groupby": ["id", "username", "email"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
query_obj = dict(**base_query_obj, extras={})
|
||||
@@ -917,7 +928,7 @@ def test_extra_cache_keys_in_sql_expression(
|
||||
"groupby": ["id", "username", "email"],
|
||||
"metrics": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
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": [],
|
||||
"columns": [],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
items: dict[str, Any] = {
|
||||
@@ -1014,7 +1025,7 @@ def test_extra_cache_keys_in_dataset_metrics_and_columns(
|
||||
"columns": ["username"],
|
||||
"metrics": ["variable_profit"],
|
||||
"is_timeseries": False,
|
||||
"filter": [],
|
||||
"filters": [],
|
||||
}
|
||||
|
||||
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(
|
||||
{
|
||||
"columns": ["col1", "col2"],
|
||||
"filter": [
|
||||
"filters": [
|
||||
{
|
||||
"col": "col5",
|
||||
"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