You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by en...@apache.org on 2026/06/18 08:49:20 UTC
(superset) branch master updated: fix(dashboard): fixed first/last aggregations in pivot tables (#33275)
This is an automated email from the ASF dual-hosted git repository.
EnxDev pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new c218dc418ba fix(dashboard): fixed first/last aggregations in pivot tables (#33275)
c218dc418ba is described below
commit c218dc418ba717a243e6e27a84e6ddcc1cd01577
Author: ksnikiforov <50...@users.noreply.github.com>
AuthorDate: Thu Jun 18 11:49:12 2026 +0300
fix(dashboard): fixed first/last aggregations in pivot tables (#33275)
Co-authored-by: Evan Rusackas <ev...@preset.io>
Co-authored-by: Amin Ghadersohi <am...@gmail.com>
Co-authored-by: Claude Fable 5 <no...@anthropic.com>
Co-authored-by: Enzo Martellucci <52...@users.noreply.github.com>
---
UPDATING.md | 4 ++
.../src/react-pivottable/utilities.ts | 24 ++-------
.../test/react-pivottable/utilities.test.ts | 61 ++++++++++++++++++++++
3 files changed, 69 insertions(+), 20 deletions(-)
diff --git a/UPDATING.md b/UPDATING.md
index e8f24360d99..01e59b4f6fb 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -24,6 +24,10 @@ assists people when migrating to a new version.
## Next
+### Pivot table First/Last aggregations follow data order
+
+The pivot table chart's `First` and `Last` aggregations now return the first and last value in data (query result) order, instead of effectively returning the minimum and maximum. Existing pivot tables that use these aggregations for totals/subtotals may show different values after upgrading. For deterministic results, ensure the underlying query has a stable sort order.
+
### `thumbnail_url` removed from dashboard list API response
The `thumbnail_url` field has been removed from `GET /api/v1/dashboard/` list responses. External consumers relying on this field must now construct the thumbnail URL client-side using `id` and `changed_on_utc`:
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts
index 2839b290ddf..0dccc885143 100644
--- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts
@@ -473,14 +473,10 @@ const baseAggregatorTemplates = {
extremes(mode: string, formatter = usFmt) {
return function ([attr]: string[]) {
- return function (data: any) {
+ return function () {
return {
val: null as any,
currencySet: new Set<string>(),
- sorter: getSort(
- typeof data !== 'undefined' ? data.sorters : null,
- attr,
- ),
push(record: PivotRecord) {
const x = record[attr];
if (['min', 'max'].includes(mode)) {
@@ -499,21 +495,9 @@ const baseAggregatorTemplates = {
this.val !== null ? this.val : coercedValue,
);
}
- } else if (
- mode === 'first' &&
- this.sorter(
- x as any,
- this.val !== null ? this.val : (x as any),
- ) <= 0
- ) {
- this.val = x;
- } else if (
- mode === 'last' &&
- this.sorter(
- x as any,
- this.val !== null ? this.val : (x as any),
- ) >= 0
- ) {
+ } else if (mode === 'first') {
+ this.val = this.val === null ? x : this.val;
+ } else if (mode === 'last') {
this.val = x;
}
if (
diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/utilities.test.ts b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/utilities.test.ts
new file mode 100644
index 00000000000..add7e37d9df
--- /dev/null
+++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/utilities.test.ts
@@ -0,0 +1,61 @@
+/**
+ * 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 { aggregators } from '../../src/react-pivottable/utilities';
+import type { PivotRecord } from '../../src/react-pivottable/utilities';
+
+// Records may legitimately carry null values for an attribute; PivotRecord only
+// models the non-null cell types, so loosen the type at the test boundary.
+type TestRecord = Record<string, string | number | boolean | null>;
+
+type ExtremesAggregator = 'First' | 'Last' | 'Minimum' | 'Maximum';
+
+// Build an `extremes`-based aggregator (First/Last/Minimum/Maximum) for `attr`
+// and feed it the records in order, returning the resulting value.
+const aggregate = (name: ExtremesAggregator, records: TestRecord[]) => {
+ const aggregator = aggregators[name](['x'])();
+ records.forEach(record => aggregator.push(record as PivotRecord));
+ return aggregator.value();
+};
+
+test('First returns the first value in data order, not the minimum', () => {
+ // Descending input: the buggy implementation returned the minimum (1).
+ expect(aggregate('First', [{ x: 5 }, { x: 3 }, { x: 1 }])).toBe(5);
+ expect(aggregate('First', [{ x: 1 }, { x: 3 }, { x: 5 }])).toBe(1);
+});
+
+test('Last returns the last value in data order, not the maximum', () => {
+ // Ascending input: the buggy implementation returned the maximum (5).
+ expect(aggregate('Last', [{ x: 1 }, { x: 3 }, { x: 5 }])).toBe(5);
+ expect(aggregate('Last', [{ x: 5 }, { x: 3 }, { x: 1 }])).toBe(1);
+});
+
+test('First keeps the first non-null value, skipping a leading null', () => {
+ expect(aggregate('First', [{ x: null }, { x: 7 }, { x: 9 }])).toBe(7);
+});
+
+test('First preserves a falsy first value such as zero', () => {
+ expect(aggregate('First', [{ x: 0 }, { x: 5 }])).toBe(0);
+});
+
+test('Minimum and Maximum still compute extremes regardless of order', () => {
+ const records = [{ x: 3 }, { x: 1 }, { x: 5 }, { x: 2 }];
+ expect(aggregate('Minimum', records)).toBe(1);
+ expect(aggregate('Maximum', records)).toBe(5);
+});