mirror of
https://github.com/apache/superset.git
synced 2026-04-10 03:45:22 +00:00
387 lines
12 KiB
TypeScript
387 lines
12 KiB
TypeScript
/**
|
|
* 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.
|
|
*/
|
|
import {
|
|
AdhocColumn,
|
|
BuildQuery,
|
|
PostProcessingRule,
|
|
QueryFormOrderBy,
|
|
QueryMode,
|
|
QueryObject,
|
|
buildQueryContext,
|
|
ensureIsArray,
|
|
getMetricLabel,
|
|
isPhysicalColumn,
|
|
removeDuplicates,
|
|
} from '@superset-ui/core';
|
|
|
|
import {
|
|
isTimeComparison,
|
|
timeCompareOperator,
|
|
} from '@superset-ui/chart-controls';
|
|
import { isEmpty } from 'lodash';
|
|
import { TableChartFormData } from './types';
|
|
import { updateTableOwnState } from './DataTable/utils/externalAPIs';
|
|
|
|
/**
|
|
* Infer query mode from form data. If `all_columns` is set, then raw records mode,
|
|
* otherwise defaults to aggregation mode.
|
|
*
|
|
* The same logic is used in `controlPanel` with control values as well.
|
|
*/
|
|
export function getQueryMode(formData: TableChartFormData) {
|
|
const { query_mode: mode } = formData;
|
|
if (mode === QueryMode.Aggregate || mode === QueryMode.Raw) {
|
|
return mode;
|
|
}
|
|
const rawColumns = formData?.all_columns;
|
|
const hasRawColumns = rawColumns && rawColumns.length > 0;
|
|
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
|
|
}
|
|
|
|
const buildQuery: BuildQuery<TableChartFormData> = (
|
|
formData: TableChartFormData,
|
|
options,
|
|
) => {
|
|
const {
|
|
percent_metrics: percentMetrics,
|
|
order_desc: orderDesc = false,
|
|
extra_form_data,
|
|
} = formData;
|
|
const queryMode = getQueryMode(formData);
|
|
const sortByMetric = ensureIsArray(formData.timeseries_limit_metric)[0];
|
|
const time_grain_sqla =
|
|
extra_form_data?.time_grain_sqla || formData.time_grain_sqla;
|
|
let formDataCopy = formData;
|
|
// never include time in raw records mode
|
|
if (queryMode === QueryMode.Raw) {
|
|
formDataCopy = {
|
|
...formData,
|
|
include_time: false,
|
|
};
|
|
}
|
|
|
|
const addComparisonPercentMetrics = (metrics: string[], suffixes: string[]) =>
|
|
metrics.reduce<string[]>((acc, metric) => {
|
|
const newMetrics = suffixes.map(suffix => `${metric}__${suffix}`);
|
|
return acc.concat([metric, ...newMetrics]);
|
|
}, []);
|
|
|
|
return buildQueryContext(formDataCopy, baseQueryObject => {
|
|
let { metrics, orderby = [], columns = [] } = baseQueryObject;
|
|
const { extras = {} } = baseQueryObject;
|
|
const postProcessing: PostProcessingRule[] = [];
|
|
const nonCustomNorInheritShifts = ensureIsArray(
|
|
formData.time_compare,
|
|
).filter((shift: string) => shift !== 'custom' && shift !== 'inherit');
|
|
const customOrInheritShifts = ensureIsArray(formData.time_compare).filter(
|
|
(shift: string) => shift === 'custom' || shift === 'inherit',
|
|
);
|
|
|
|
let timeOffsets: string[] = [];
|
|
|
|
// Shifts for non-custom or non inherit time comparison
|
|
if (
|
|
isTimeComparison(formData, baseQueryObject) &&
|
|
!isEmpty(nonCustomNorInheritShifts)
|
|
) {
|
|
timeOffsets = nonCustomNorInheritShifts;
|
|
}
|
|
|
|
// Shifts for custom or inherit time comparison
|
|
if (
|
|
isTimeComparison(formData, baseQueryObject) &&
|
|
!isEmpty(customOrInheritShifts)
|
|
) {
|
|
if (customOrInheritShifts.includes('custom')) {
|
|
timeOffsets = timeOffsets.concat([formData.start_date_offset]);
|
|
}
|
|
if (customOrInheritShifts.includes('inherit')) {
|
|
timeOffsets = timeOffsets.concat(['inherit']);
|
|
}
|
|
}
|
|
|
|
if (
|
|
extra_form_data?.time_compare &&
|
|
!timeOffsets.includes(extra_form_data.time_compare)
|
|
) {
|
|
timeOffsets = [extra_form_data.time_compare];
|
|
}
|
|
|
|
let temporalColumnAdded = false;
|
|
let temporalColumn = null;
|
|
|
|
if (queryMode === QueryMode.Aggregate) {
|
|
metrics = metrics || [];
|
|
// override orderby with timeseries metric when in aggregation mode
|
|
if (sortByMetric) {
|
|
orderby = [[sortByMetric, !orderDesc]];
|
|
} else if (metrics?.length > 0) {
|
|
// default to ordering by first metric in descending order
|
|
// when no "sort by" metric is set (regardless if "SORT DESC" is set to true)
|
|
orderby = [[metrics[0], false]];
|
|
}
|
|
// add postprocessing for percent metrics only when in aggregation mode
|
|
type PercentMetricCalculationMode = 'row_limit' | 'all_records';
|
|
|
|
const calculationMode: PercentMetricCalculationMode =
|
|
(formData.percent_metric_calculation as PercentMetricCalculationMode) ||
|
|
'row_limit';
|
|
|
|
if (percentMetrics && percentMetrics.length > 0) {
|
|
const percentMetricsLabelsWithTimeComparison = isTimeComparison(
|
|
formData,
|
|
baseQueryObject,
|
|
)
|
|
? addComparisonPercentMetrics(
|
|
percentMetrics.map(getMetricLabel),
|
|
timeOffsets,
|
|
)
|
|
: percentMetrics.map(getMetricLabel);
|
|
|
|
const percentMetricLabels = removeDuplicates(
|
|
percentMetricsLabelsWithTimeComparison,
|
|
);
|
|
metrics = removeDuplicates(
|
|
metrics.concat(percentMetrics),
|
|
getMetricLabel,
|
|
);
|
|
|
|
if (calculationMode === 'all_records') {
|
|
postProcessing.push({
|
|
operation: 'contribution',
|
|
options: {
|
|
columns: percentMetricLabels,
|
|
rename_columns: percentMetricLabels.map(m => `%${m}`),
|
|
},
|
|
});
|
|
} else {
|
|
postProcessing.push({
|
|
operation: 'contribution',
|
|
options: {
|
|
columns: percentMetricLabels,
|
|
rename_columns: percentMetricLabels.map(m => `%${m}`),
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add the operator for the time comparison if some is selected
|
|
if (!isEmpty(timeOffsets)) {
|
|
postProcessing.push(timeCompareOperator(formData, baseQueryObject));
|
|
}
|
|
|
|
const temporalColumnsLookup = formData?.temporal_columns_lookup;
|
|
// Filter out the column if needed and prepare the temporal column object
|
|
|
|
columns = columns.filter(col => {
|
|
const shouldBeAdded =
|
|
isPhysicalColumn(col) &&
|
|
time_grain_sqla &&
|
|
temporalColumnsLookup?.[col];
|
|
|
|
if (shouldBeAdded && !temporalColumnAdded) {
|
|
temporalColumn = {
|
|
timeGrain: time_grain_sqla,
|
|
columnType: 'BASE_AXIS',
|
|
sqlExpression: col,
|
|
label: col,
|
|
expressionType: 'SQL',
|
|
} as AdhocColumn;
|
|
temporalColumnAdded = true;
|
|
return false; // Do not include this in the output; it's added separately
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// So we ensure the temporal column is added first
|
|
if (temporalColumn) {
|
|
columns = [temporalColumn, ...columns];
|
|
}
|
|
}
|
|
|
|
const moreProps: Partial<QueryObject> = {};
|
|
const ownState = options?.ownState ?? {};
|
|
// Build Query flag to check if its for either download as csv, excel or json
|
|
const isDownloadQuery =
|
|
['csv', 'xlsx'].includes(formData?.result_format || '') ||
|
|
(formData?.result_format === 'json' &&
|
|
formData?.result_type === 'results');
|
|
|
|
if (isDownloadQuery) {
|
|
moreProps.row_limit = Number(formDataCopy.row_limit) || 0;
|
|
moreProps.row_offset = 0;
|
|
}
|
|
|
|
if (!isDownloadQuery && formDataCopy.server_pagination) {
|
|
const pageSize = ownState.pageSize ?? formDataCopy.server_page_length;
|
|
const currentPage = ownState.currentPage ?? 0;
|
|
|
|
moreProps.row_limit = pageSize;
|
|
moreProps.row_offset = currentPage * pageSize;
|
|
}
|
|
|
|
// getting sort by in case of server pagination from own state
|
|
let sortByFromOwnState: QueryFormOrderBy[] | undefined;
|
|
if (Array.isArray(ownState?.sortBy) && ownState?.sortBy.length > 0) {
|
|
const sortByItem = ownState?.sortBy[0];
|
|
sortByFromOwnState = [[sortByItem?.key, !sortByItem?.desc]];
|
|
}
|
|
|
|
let queryObject = {
|
|
...baseQueryObject,
|
|
columns,
|
|
extras,
|
|
orderby:
|
|
formData.server_pagination && sortByFromOwnState
|
|
? sortByFromOwnState
|
|
: orderby,
|
|
metrics,
|
|
post_processing: postProcessing,
|
|
time_offsets: timeOffsets,
|
|
...moreProps,
|
|
};
|
|
|
|
if (
|
|
formData.server_pagination &&
|
|
options?.extras?.cachedChanges?.[formData.slice_id] &&
|
|
JSON.stringify(options?.extras?.cachedChanges?.[formData.slice_id]) !==
|
|
JSON.stringify(queryObject.filters)
|
|
) {
|
|
queryObject = { ...queryObject, row_offset: 0 };
|
|
const modifiedOwnState = {
|
|
...options?.ownState,
|
|
currentPage: 0,
|
|
pageSize: queryObject.row_limit ?? 0,
|
|
};
|
|
updateTableOwnState(options?.hooks?.setDataMask, modifiedOwnState);
|
|
}
|
|
|
|
if (formData.server_pagination) {
|
|
// Add search filter if search text exists
|
|
if (ownState.searchText && ownState?.searchColumn) {
|
|
queryObject = {
|
|
...queryObject,
|
|
filters: [
|
|
...(queryObject.filters || []),
|
|
{
|
|
col: ownState?.searchColumn,
|
|
op: 'ILIKE',
|
|
val: `${ownState.searchText}%`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
// Because we use same buildQuery for all table on the page we need split them by id
|
|
options?.hooks?.setCachedChanges({
|
|
[formData.slice_id]: queryObject.filters,
|
|
});
|
|
|
|
const extraQueries: QueryObject[] = [];
|
|
|
|
const calculationMode = formData.percent_metric_calculation || 'row_limit';
|
|
|
|
if (
|
|
calculationMode === 'all_records' &&
|
|
percentMetrics &&
|
|
percentMetrics.length > 0
|
|
) {
|
|
extraQueries.push({
|
|
...queryObject,
|
|
columns: [],
|
|
metrics: percentMetrics,
|
|
post_processing: [],
|
|
row_limit: 0,
|
|
row_offset: 0,
|
|
orderby: [],
|
|
is_timeseries: false,
|
|
});
|
|
}
|
|
|
|
if (
|
|
metrics?.length &&
|
|
formData.show_totals &&
|
|
queryMode === QueryMode.Aggregate
|
|
) {
|
|
extraQueries.push({
|
|
...queryObject,
|
|
columns: [],
|
|
row_limit: 0,
|
|
row_offset: 0,
|
|
post_processing: [],
|
|
order_desc: undefined,
|
|
orderby: undefined,
|
|
});
|
|
}
|
|
|
|
const interactiveGroupBy = formData.extra_form_data?.interactive_groupby;
|
|
if (interactiveGroupBy && queryObject.columns) {
|
|
queryObject.columns = [
|
|
...new Set([...queryObject.columns, ...interactiveGroupBy]),
|
|
];
|
|
}
|
|
|
|
// Now since row limit control is always visible even
|
|
// in case of server pagination
|
|
// we must use row limit from form data
|
|
if (formData.server_pagination && !isDownloadQuery) {
|
|
return [
|
|
{ ...queryObject },
|
|
{
|
|
...queryObject,
|
|
time_offsets: [],
|
|
row_limit: Number(formData?.row_limit ?? 0),
|
|
row_offset: 0,
|
|
post_processing: [],
|
|
is_rowcount: true,
|
|
},
|
|
...extraQueries,
|
|
];
|
|
}
|
|
|
|
return [queryObject, ...extraQueries];
|
|
});
|
|
};
|
|
|
|
// Use this closure to cache changing of external filters, if we have server pagination we need reset page to 0, after
|
|
// external filter changed
|
|
export const cachedBuildQuery = (): BuildQuery<TableChartFormData> => {
|
|
let cachedChanges: any = {};
|
|
const setCachedChanges = (newChanges: any) => {
|
|
cachedChanges = { ...cachedChanges, ...newChanges };
|
|
};
|
|
|
|
return (formData, options) =>
|
|
buildQuery(
|
|
{ ...formData },
|
|
{
|
|
extras: { cachedChanges },
|
|
ownState: options?.ownState ?? {},
|
|
hooks: {
|
|
...options?.hooks,
|
|
setDataMask: () => {},
|
|
setCachedChanges,
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
export default cachedBuildQuery();
|