mirror of
https://github.com/apache/superset.git
synced 2026-08-25 17:41:14 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3585e8235a | ||
|
|
34ffa37aaa | ||
|
|
02b3e43b66 | ||
|
|
f9530f31ab |
@@ -53,12 +53,6 @@ jobs:
|
||||
python-version: ${{ github.event_name == 'pull_request' && fromJSON('["current"]') || fromJSON('["current", "next"]') }}
|
||||
env:
|
||||
PYTHONPATH: ${{ github.workspace }}
|
||||
# Promotes the SQLAlchemy 2.0 deprecation warnings already locked in as
|
||||
# errors via pytest.ini's `filterwarnings` to actually run in CI, so a
|
||||
# regression on those fails the build instead of relying on a
|
||||
# contributor remembering to set this locally. See the migration
|
||||
# battleplan: https://github.com/apache/superset/discussions/40273
|
||||
SQLALCHEMY_WARN_20: "1"
|
||||
steps:
|
||||
- name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )"
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
+28
-1
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { QueryFormMetric } from '@superset-ui/core';
|
||||
import { getTotalsMetrics } from './getTotalsMetrics';
|
||||
import { getTotalsMetrics, toTotalsAggregate } from './getTotalsMetrics';
|
||||
|
||||
const simpleMetric = (aggregate: string): QueryFormMetric =>
|
||||
({
|
||||
@@ -76,4 +76,31 @@ describe('getTotalsMetrics', () => {
|
||||
test('returns an empty array when given no metrics', () => {
|
||||
expect(getTotalsMetrics([], 'AVG')).toEqual([]);
|
||||
});
|
||||
|
||||
test("ORIGINAL keeps each metric's own aggregate", () => {
|
||||
const metrics = [
|
||||
simpleMetric('COUNT_DISTINCT'),
|
||||
sqlMetric(),
|
||||
savedMetric(),
|
||||
];
|
||||
const result = getTotalsMetrics(metrics, 'ORIGINAL');
|
||||
|
||||
expect(result).toBe(metrics);
|
||||
expect(result[0]).toEqual(
|
||||
expect.objectContaining({ aggregate: 'COUNT_DISTINCT' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toTotalsAggregate', () => {
|
||||
test.each(['SUM', 'AVG'] as const)('passes %s through', value => {
|
||||
expect(toTotalsAggregate(value)).toBe(value);
|
||||
});
|
||||
|
||||
test.each([undefined, null, '', 'MEDIAN', 'sum'])(
|
||||
'falls back to ORIGINAL for %p',
|
||||
value => {
|
||||
expect(toTotalsAggregate(value)).toBe('ORIGINAL');
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+31
-11
@@ -18,26 +18,46 @@
|
||||
*/
|
||||
import { isAdhocMetricSimple, QueryFormMetric } from '@superset-ui/core';
|
||||
|
||||
export type TotalsAggregate = 'SUM' | 'AVG';
|
||||
/**
|
||||
* How the "Show summary" totals row aggregates each metric.
|
||||
*
|
||||
* ``ORIGINAL`` keeps every metric's own aggregation. It is the default because
|
||||
* overriding is not universally valid: ``SUM`` over a ``COUNT_DISTINCT`` of a
|
||||
* non-numeric column (a uuid, say) is rejected outright by the database, and
|
||||
* over a numeric id column it silently produces a meaningless number.
|
||||
*/
|
||||
export type TotalsAggregate = 'ORIGINAL' | 'SUM' | 'AVG';
|
||||
|
||||
/**
|
||||
* Build the metrics for a chart's "Show summary" totals query, overriding
|
||||
* each Simple (adhoc) metric's aggregate function with the user-chosen
|
||||
* totals aggregate. The totals query has no GROUP BY, so the database
|
||||
* evaluates each metric fresh over all rows -- swapping the aggregate here
|
||||
* is a correct, independent computation, not a re-aggregation of
|
||||
* already-aggregated per-row values.
|
||||
* Build the metrics for a chart's "Show summary" totals query.
|
||||
*
|
||||
* Custom-SQL metrics and saved (string) metrics pass through unchanged:
|
||||
* there is no safe way to rewrite an arbitrary SQL expression's aggregate
|
||||
* function without parsing it, so the totals row keeps their own native
|
||||
* aggregate for those.
|
||||
* With SUM or AVG, each Simple (adhoc) metric is cloned with its aggregate
|
||||
* replaced. The totals query has no GROUP BY, so the database evaluates each
|
||||
* metric fresh over all rows -- that swap is an independent computation, not a
|
||||
* re-aggregation of already-aggregated per-row values.
|
||||
*
|
||||
* Custom-SQL and saved (string) metrics always pass through unchanged: there is
|
||||
* no safe way to rewrite an arbitrary SQL expression's aggregate without
|
||||
* parsing it, so the totals row keeps their own native aggregate.
|
||||
*/
|
||||
export function getTotalsMetrics(
|
||||
metrics: QueryFormMetric[],
|
||||
aggregate: TotalsAggregate,
|
||||
): QueryFormMetric[] {
|
||||
if (aggregate === 'ORIGINAL') {
|
||||
return metrics;
|
||||
}
|
||||
return metrics.map(metric =>
|
||||
isAdhocMetricSimple(metric) ? { ...metric, aggregate } : metric,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow a raw ``totals_aggregate`` form-data value to a TotalsAggregate.
|
||||
*
|
||||
* Anything other than an explicit SUM/AVG — including charts saved before the
|
||||
* control existed — keeps each metric's own aggregation.
|
||||
*/
|
||||
export function toTotalsAggregate(value: unknown): TotalsAggregate {
|
||||
return value === 'SUM' || value === 'AVG' ? value : 'ORIGINAL';
|
||||
}
|
||||
|
||||
+2
-3
@@ -64,9 +64,8 @@ export default function createSmartNumberFormatter(
|
||||
description,
|
||||
formatFunc: value => `${getSign(value)}${formatValue(value)}`,
|
||||
id:
|
||||
id || signed
|
||||
? NumberFormats.SMART_NUMBER_SIGNED
|
||||
: NumberFormats.SMART_NUMBER,
|
||||
id ??
|
||||
(signed ? NumberFormats.SMART_NUMBER_SIGNED : NumberFormats.SMART_NUMBER),
|
||||
label: label ?? 'Adaptive formatter',
|
||||
});
|
||||
}
|
||||
|
||||
+6
@@ -24,6 +24,12 @@ describe('createSmartNumberFormatter(options)', () => {
|
||||
const formatter = createSmartNumberFormatter();
|
||||
expect(formatter).toBeInstanceOf(NumberFormatter);
|
||||
});
|
||||
test('uses the supplied formatter id regardless of signed option', () => {
|
||||
expect(createSmartNumberFormatter({ id: 'custom' }).id).toBe('custom');
|
||||
expect(
|
||||
createSmartNumberFormatter({ id: 'custom-signed', signed: true }).id,
|
||||
).toBe('custom-signed');
|
||||
});
|
||||
describe('using default options', () => {
|
||||
const formatter = createSmartNumberFormatter();
|
||||
test('formats 0 correctly', () => {
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
getTotalsMetrics,
|
||||
isTimeComparison,
|
||||
timeCompareOperator,
|
||||
TotalsAggregate,
|
||||
toTotalsAggregate,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { TableChartFormData } from './types';
|
||||
@@ -696,13 +696,16 @@ export const buildQueryUncached: BuildQuery<TableChartFormData> = (
|
||||
formData.show_totals &&
|
||||
queryMode === QueryMode.Aggregate,
|
||||
);
|
||||
const totalsAggregate: TotalsAggregate =
|
||||
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
|
||||
const totalsAggregate = toTotalsAggregate(formData.totals_aggregate);
|
||||
// Raw-mode summary columns have no metric of their own to preserve, so
|
||||
// ORIGINAL has nothing to fall back to; sum them as before.
|
||||
const rawSummaryAggregate =
|
||||
totalsAggregate === 'ORIGINAL' ? 'SUM' : totalsAggregate;
|
||||
const totalsMetrics =
|
||||
rawSummaryColumns.length > 0
|
||||
? rawSummaryColumns.map(columnName => ({
|
||||
expressionType: 'SIMPLE' as const,
|
||||
aggregate: totalsAggregate,
|
||||
aggregate: rawSummaryAggregate,
|
||||
column: { column_name: columnName },
|
||||
label: columnName,
|
||||
}))
|
||||
|
||||
@@ -503,14 +503,18 @@ const config: ControlPanelConfig = {
|
||||
label: t('Summary aggregation'),
|
||||
renderTrigger: true,
|
||||
description: t(
|
||||
'Aggregation used for the summary row, independent of each ' +
|
||||
"metric's own aggregation. Only applies to simple metrics " +
|
||||
'(a metric built from custom SQL keeps its own aggregation ' +
|
||||
'in the summary row).',
|
||||
'Aggregation used for the summary row. By default each metric ' +
|
||||
'keeps its own aggregation; Sum and Average override it for ' +
|
||||
'the summary row only. The override applies to simple ' +
|
||||
'metrics (a metric built from custom SQL always keeps its ' +
|
||||
'own aggregation). Overriding a count or a distinct count ' +
|
||||
'sums the counted column instead, which fails outright on a ' +
|
||||
'non-numeric column.',
|
||||
),
|
||||
default: 'SUM',
|
||||
default: 'ORIGINAL',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['ORIGINAL', t("Each metric's own")],
|
||||
['SUM', t('Sum')],
|
||||
['AVG', t('Average')],
|
||||
],
|
||||
|
||||
@@ -1561,7 +1561,7 @@ describe('plugin-chart-ag-grid-table', () => {
|
||||
expect(queries[1].metrics).toEqual(['count']);
|
||||
});
|
||||
|
||||
test('defaults aggregate-mode totals to SUM for a simple metric', () => {
|
||||
test("defaults aggregate-mode totals to the metric's own aggregate", () => {
|
||||
const simpleMetric = {
|
||||
expressionType: 'SIMPLE' as const,
|
||||
column: { column_name: 'sales' },
|
||||
@@ -1580,9 +1580,29 @@ describe('plugin-chart-ag-grid-table', () => {
|
||||
{ ownState: {} },
|
||||
);
|
||||
|
||||
expect(queries[1].metrics).toEqual([
|
||||
{ ...simpleMetric, aggregate: 'SUM' },
|
||||
]);
|
||||
expect(queries[1].metrics).toEqual([simpleMetric]);
|
||||
});
|
||||
|
||||
test('keeps COUNT_DISTINCT in aggregate-mode totals by default', () => {
|
||||
const countDistinctMetric = {
|
||||
expressionType: 'SIMPLE' as const,
|
||||
column: { column_name: 'contract_id' },
|
||||
aggregate: 'COUNT_DISTINCT' as const,
|
||||
label: 'contracts',
|
||||
};
|
||||
const { queries } = buildQuery(
|
||||
{
|
||||
viz_type: VizType.Table,
|
||||
datasource: '11__table',
|
||||
query_mode: QueryMode.Aggregate,
|
||||
groupby: ['state'],
|
||||
metrics: [countDistinctMetric],
|
||||
show_totals: true,
|
||||
},
|
||||
{ ownState: {} },
|
||||
);
|
||||
|
||||
expect(queries[1].metrics).toEqual([countDistinctMetric]);
|
||||
});
|
||||
|
||||
test('overrides aggregate-mode totals to AVG for a simple metric when totals_aggregate is set', () => {
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
getTotalsMetrics,
|
||||
isTimeComparison,
|
||||
timeCompareOperator,
|
||||
TotalsAggregate,
|
||||
toTotalsAggregate,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import { TableChartFormData } from './types';
|
||||
@@ -349,8 +349,7 @@ export const buildQuery: BuildQuery<TableChartFormData> = (
|
||||
formData.show_totals &&
|
||||
queryMode === QueryMode.Aggregate
|
||||
) {
|
||||
const totalsAggregate: TotalsAggregate =
|
||||
formData.totals_aggregate === 'AVG' ? 'AVG' : 'SUM';
|
||||
const totalsAggregate = toTotalsAggregate(formData.totals_aggregate);
|
||||
extraQueries.push({
|
||||
...queryObject,
|
||||
columns: [],
|
||||
|
||||
@@ -475,14 +475,18 @@ const config: ControlPanelConfig = {
|
||||
type: 'SelectControl',
|
||||
label: t('Summary aggregation'),
|
||||
description: t(
|
||||
'Aggregation used for the summary row, independent of each ' +
|
||||
"metric's own aggregation. Only applies to simple metrics " +
|
||||
'(a metric built from custom SQL keeps its own aggregation ' +
|
||||
'in the summary row).',
|
||||
'Aggregation used for the summary row. By default each metric ' +
|
||||
'keeps its own aggregation; Sum and Average override it for ' +
|
||||
'the summary row only. The override applies to simple ' +
|
||||
'metrics (a metric built from custom SQL always keeps its ' +
|
||||
'own aggregation). Overriding a count or a distinct count ' +
|
||||
'sums the counted column instead, which fails outright on a ' +
|
||||
'non-numeric column.',
|
||||
),
|
||||
default: 'SUM',
|
||||
default: 'ORIGINAL',
|
||||
clearable: false,
|
||||
choices: [
|
||||
['ORIGINAL', t("Each metric's own")],
|
||||
['SUM', t('Sum')],
|
||||
['AVG', t('Average')],
|
||||
],
|
||||
|
||||
@@ -340,7 +340,7 @@ describe('plugin-chart-table', () => {
|
||||
label: 'sum_sales',
|
||||
};
|
||||
|
||||
test('defaults the totals query metric aggregate to SUM', () => {
|
||||
test("defaults to each metric's own aggregate", () => {
|
||||
const { queries } = buildQueryCached({
|
||||
...basicFormData,
|
||||
query_mode: QueryMode.Aggregate,
|
||||
@@ -350,9 +350,28 @@ describe('plugin-chart-table', () => {
|
||||
});
|
||||
|
||||
expect(queries).toHaveLength(2);
|
||||
expect(queries[1].metrics).toEqual([
|
||||
{ ...simpleMetric, aggregate: 'SUM' },
|
||||
]);
|
||||
expect(queries[1].metrics).toEqual([simpleMetric]);
|
||||
});
|
||||
|
||||
test('keeps COUNT_DISTINCT in the summary row by default', () => {
|
||||
// Overriding this to SUM sums the counted column instead of counting
|
||||
// it, which is meaningless on a numeric id and is rejected outright by
|
||||
// the database on a non-numeric one (e.g. a uuid).
|
||||
const countDistinctMetric = {
|
||||
expressionType: 'SIMPLE' as const,
|
||||
column: { column_name: 'contract_id' },
|
||||
aggregate: 'COUNT_DISTINCT' as const,
|
||||
label: 'contracts',
|
||||
};
|
||||
const { queries } = buildQueryCached({
|
||||
...basicFormData,
|
||||
query_mode: QueryMode.Aggregate,
|
||||
metrics: [countDistinctMetric],
|
||||
groupby: ['category'],
|
||||
show_totals: true,
|
||||
});
|
||||
|
||||
expect(queries[1].metrics).toEqual([countDistinctMetric]);
|
||||
});
|
||||
|
||||
test('overrides simple metric aggregate with totals_aggregate for the summary query only', () => {
|
||||
|
||||
@@ -379,37 +379,37 @@ msgstr "虛擬"
|
||||
msgid "%s aggregates(s)"
|
||||
msgstr "%s 聚合"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s column"
|
||||
msgid_plural "%s columns"
|
||||
msgstr[0] "%s 列"
|
||||
msgstr[0] "%s 個欄位"
|
||||
|
||||
#, python-format
|
||||
msgid "%s column(s)"
|
||||
msgstr "%s 列"
|
||||
msgstr "%s 個欄位"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s day ago"
|
||||
msgid_plural "%s days ago"
|
||||
msgstr[0] "1 天之前"
|
||||
msgstr[0] "%s 天前"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s hr ago"
|
||||
msgid_plural "%s hr ago"
|
||||
msgstr[0] "%s 行"
|
||||
msgstr[0] "%s 小時前"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s imported"
|
||||
msgstr "數據集已導入"
|
||||
msgstr "已匯入 %s"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s item"
|
||||
msgid_plural "%s items"
|
||||
msgstr[0] "%s 個選項"
|
||||
msgstr[0] "%s 個項目"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s item(s)"
|
||||
msgstr "%s 個選項"
|
||||
msgstr "%s 個項目"
|
||||
|
||||
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: ar, cs, de,
|
||||
# es, fa, fr, ja, lv, mi, nl, pl, pt_BR, ru, sk, sl, sr, sr_Latn, tr, uk]
|
||||
@@ -419,15 +419,15 @@ msgid ""
|
||||
"all selected objects."
|
||||
msgstr "%s 個項目無法標記,因為您對所有選取的物件沒有編輯權限。"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s metric"
|
||||
msgid_plural "%s metrics"
|
||||
msgstr[0] "排序指標"
|
||||
msgstr[0] "%s 個指標"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s min ago"
|
||||
msgid_plural "%s min ago"
|
||||
msgstr[0] "月"
|
||||
msgstr[0] "%s 分鐘前"
|
||||
|
||||
#, python-format
|
||||
msgid ""
|
||||
@@ -451,47 +451,47 @@ msgstr[0] "%s 個選項"
|
||||
msgid "%s option(s)"
|
||||
msgstr "%s 個選項"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s out of %s column"
|
||||
msgid_plural "%s out of %s columns"
|
||||
msgstr[0] "自定義列"
|
||||
msgstr[0] "已選取 %s/%s 個欄位"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s out of %s metric"
|
||||
msgid_plural "%s out of %s metrics"
|
||||
msgstr[0] "排序指標"
|
||||
msgstr[0] "已選取 %s/%s 個指標"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s out of %s selected"
|
||||
msgstr "%s 已選定"
|
||||
msgstr "已選取 %s/%s 個項目"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s recipients"
|
||||
msgstr "%s 最近"
|
||||
msgstr "%s 收件者"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s record..."
|
||||
msgid_plural "%s records..."
|
||||
msgstr[0] "%s 異常"
|
||||
msgstr[0] "%s 筆記錄..."
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s row"
|
||||
msgid_plural "%s rows"
|
||||
msgstr[0] "%s 行"
|
||||
msgstr[0] "%s 列"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s s ago"
|
||||
msgid_plural "%s s ago"
|
||||
msgstr[0] "30 天之前"
|
||||
msgstr[0] "%s 秒前"
|
||||
|
||||
#, python-format
|
||||
msgid "%s saved metric(s)"
|
||||
msgstr "%s 保存的指標"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s second"
|
||||
msgid_plural "%s seconds"
|
||||
msgstr[0] "5 秒"
|
||||
msgstr[0] "%s 秒"
|
||||
|
||||
# Machine-translated via backfill_po.py (claude-sonnet-4-6) [refs: de, es, sr,
|
||||
# sr_Latn]
|
||||
@@ -511,13 +511,13 @@ msgstr "%s 個語意檢視新增失敗"
|
||||
msgid "%s semantic view(s) failed to add: %s"
|
||||
msgstr "%s 個語意檢視新增失敗:%s"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s tab selected"
|
||||
msgstr "%s 已選定"
|
||||
msgstr "已選取「%s」分頁"
|
||||
|
||||
#, fuzzy, python-format
|
||||
#, python-format
|
||||
msgid "%s updated"
|
||||
msgstr "上次更新 %s"
|
||||
msgstr "已更新 %s"
|
||||
|
||||
#, python-format
|
||||
msgid "%s%s"
|
||||
@@ -677,13 +677,11 @@ msgstr "每年年初的頻率"
|
||||
msgid "10 minute"
|
||||
msgstr "10 分鐘"
|
||||
|
||||
#, fuzzy
|
||||
msgid "10 seconds"
|
||||
msgstr "30 秒"
|
||||
msgstr "10 秒"
|
||||
|
||||
#, fuzzy
|
||||
msgid "10/90 percentiles"
|
||||
msgstr "9/91 百分位"
|
||||
msgstr "10/90 百分位數"
|
||||
|
||||
#. do-not-translate
|
||||
msgid "10000"
|
||||
@@ -696,9 +694,8 @@ msgstr "週"
|
||||
msgid "104 weeks ago"
|
||||
msgstr "104 週之前"
|
||||
|
||||
#, fuzzy
|
||||
msgid "12 hours"
|
||||
msgstr "1 小時"
|
||||
msgstr "12 小時"
|
||||
|
||||
msgid "15 minute"
|
||||
msgstr "15 分鐘"
|
||||
@@ -761,9 +758,8 @@ msgstr "2/98 百分位"
|
||||
msgid "22"
|
||||
msgstr "22"
|
||||
|
||||
#, fuzzy
|
||||
msgid "24 hours"
|
||||
msgstr "6 小時"
|
||||
msgstr "24 小時"
|
||||
|
||||
#, fuzzy
|
||||
msgid "28 days"
|
||||
@@ -831,9 +827,8 @@ msgstr "5 秒"
|
||||
msgid "5 seconds"
|
||||
msgstr "5 秒"
|
||||
|
||||
#, fuzzy
|
||||
msgid "5/95 percentiles"
|
||||
msgstr "9/91 百分位"
|
||||
msgstr "5/95 百分位數"
|
||||
|
||||
#, fuzzy
|
||||
msgid "52 weeks"
|
||||
|
||||
Reference in New Issue
Block a user