codeant-ai-for-open-source[bot] commented on code in PR #41184:
URL: https://github.com/apache/superset/pull/41184#discussion_r3555846271
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts:
##########
@@ -64,36 +63,73 @@ const chartProps = new ChartProps<QueryFormData>({
theme: supersetTheme,
});
-test('should transform chart props for viz', () => {
- expect(transformProps(chartProps)).toEqual({
- width: 800,
- height: 600,
- groupbyRows: ['row1', 'row2'],
- groupbyColumns: ['col1', 'col2'],
- metrics: ['metric1', 'metric2'],
- tableRenderer: 'Table With Subtotal',
- colOrder: 'key_a_to_z',
- rowOrder: 'key_a_to_z',
- aggregateFunction: 'Sum',
- transposePivot: true,
- combineMetric: true,
- rowSubtotalPosition: true,
- colSubtotalPosition: true,
+test('should pass through formData props for viz', () => {
+ const result = transformProps(chartProps) as any;
+ expect(result.width).toBe(800);
+ expect(result.height).toBe(600);
+ expect(result.groupbyRows).toEqual(['row1', 'row2']);
+ expect(result.groupbyColumns).toEqual(['col1', 'col2']);
+ expect(result.metrics).toEqual(['metric1', 'metric2']);
+ expect(result.metricsLayout).toBe(MetricsLayoutEnum.COLUMNS);
+ expect(result.currencyFormat).toEqual({
+ symbol: 'USD',
+ symbolPosition: 'prefix',
+ });
+ // data is the per-level QueryData[] (split/synthesized), not raw rows.
+ expect(Array.isArray(result.data)).toBe(true);
+ result.data.forEach((level: any) => {
Review Comment:
**Suggestion:** Type the callback parameter with the appropriate
level/query-data interface instead of `any`. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The callback parameter is explicitly typed as `any` in the changed test
code. That is a direct violation of the rule against `any` in new or changed
TypeScript code.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=1c274faafa0f4087954bc9846361baf4&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=1c274faafa0f4087954bc9846361baf4&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
**Line:** 80:80
**Comment:**
*Custom Rule: Type the callback parameter with the appropriate
level/query-data interface instead of `any`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=b3ef61008e30676a38c50270548841381d9765a448409e9419fc4dcce3d2bfb0&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=b3ef61008e30676a38c50270548841381d9765a448409e9419fc4dcce3d2bfb0&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts:
##########
@@ -64,36 +63,73 @@ const chartProps = new ChartProps<QueryFormData>({
theme: supersetTheme,
});
-test('should transform chart props for viz', () => {
- expect(transformProps(chartProps)).toEqual({
- width: 800,
- height: 600,
- groupbyRows: ['row1', 'row2'],
- groupbyColumns: ['col1', 'col2'],
- metrics: ['metric1', 'metric2'],
- tableRenderer: 'Table With Subtotal',
- colOrder: 'key_a_to_z',
- rowOrder: 'key_a_to_z',
- aggregateFunction: 'Sum',
- transposePivot: true,
- combineMetric: true,
- rowSubtotalPosition: true,
- colSubtotalPosition: true,
+test('should pass through formData props for viz', () => {
+ const result = transformProps(chartProps) as any;
Review Comment:
**Suggestion:** Replace this `any` cast with the concrete transform result
type so the assertions remain type-safe. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The updated TypeScript test still uses `as any` on the transform result.
This directly violates the no-any-types rule, so the suggestion correctly
identifies a real issue.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=baea13d67da44e03b6b84bec65456e6b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=baea13d67da44e03b6b84bec65456e6b&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
**Line:** 67:67
**Comment:**
*Custom Rule: Replace this `any` cast with the concrete transform
result type so the assertions remain type-safe.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=6ada9c9e6dba403279bfbee20cf5146b070d205d8b26d4e263297f9e0c48784f&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=6ada9c9e6dba403279bfbee20cf5146b070d205d8b26d4e263297f9e0c48784f&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts:
##########
@@ -64,36 +63,73 @@ const chartProps = new ChartProps<QueryFormData>({
theme: supersetTheme,
});
-test('should transform chart props for viz', () => {
- expect(transformProps(chartProps)).toEqual({
- width: 800,
- height: 600,
- groupbyRows: ['row1', 'row2'],
- groupbyColumns: ['col1', 'col2'],
- metrics: ['metric1', 'metric2'],
- tableRenderer: 'Table With Subtotal',
- colOrder: 'key_a_to_z',
- rowOrder: 'key_a_to_z',
- aggregateFunction: 'Sum',
- transposePivot: true,
- combineMetric: true,
- rowSubtotalPosition: true,
- colSubtotalPosition: true,
+test('should pass through formData props for viz', () => {
+ const result = transformProps(chartProps) as any;
+ expect(result.width).toBe(800);
+ expect(result.height).toBe(600);
+ expect(result.groupbyRows).toEqual(['row1', 'row2']);
+ expect(result.groupbyColumns).toEqual(['col1', 'col2']);
+ expect(result.metrics).toEqual(['metric1', 'metric2']);
+ expect(result.metricsLayout).toBe(MetricsLayoutEnum.COLUMNS);
+ expect(result.currencyFormat).toEqual({
+ symbol: 'USD',
+ symbolPosition: 'prefix',
+ });
+ // data is the per-level QueryData[] (split/synthesized), not raw rows.
+ expect(Array.isArray(result.data)).toBe(true);
+ result.data.forEach((level: any) => {
+ expect(level).toHaveProperty('groupby');
+ expect(level).toHaveProperty('data');
+ });
+});
+
+test('non-additive: transformProps splits the GROUPING SETS result by level',
() => {
+ const gm = (col: string) => `${col}__superset_grouping`;
+ const localFormData = {
+ ...formData,
+ combineMetric: false,
+ transposePivot: false,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ groupbyRows: ['region'],
+ groupbyColumns: [],
colTotals: true,
rowTotals: true,
- valueFormat: 'SMART_NUMBER',
- data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
- setDataMask,
- selectedFilters: {},
- verboseMap: {},
- metricsLayout: MetricsLayoutEnum.COLUMNS,
- metricColorFormatters: [],
- dateFormatters: {},
- emitCrossFilters: false,
- columnFormats: {},
- currencyFormats: {},
- currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+ metrics: ['m'], // saved-metric string -> non-additive
+ };
+ const cp = new ChartProps<QueryFormData>({
+ formData: localFormData as unknown as QueryFormData,
+ width: 800,
+ height: 600,
+ queriesData: [
+ {
+ // One combined GROUPING SETS result: leaf rows (region marker 0) +
+ // grand total row (region marker 1).
+ data: [
+ { region: 'US', m: 10, [gm('region')]: 0 },
+ { region: 'EU', m: 5, [gm('region')]: 0 },
+ { region: null, m: 15, [gm('region')]: 1 },
+ ],
+ colnames: ['region', 'm', gm('region')],
+ coltypes: [1, 0, 0],
+ },
+ ],
+ hooks: { setDataMask },
+ filterState: { selectedFilters: {} },
+ datasource: { verboseMap: {}, columnFormats: {} },
+ theme: supersetTheme,
});
+
+ const result = transformProps(cp) as any;
+ const grand = result.data.find(
+ (d: any) => d.groupby.rows.length === 0 && d.groupby.columns.length === 0,
Review Comment:
**Suggestion:** Give the predicate parameter a concrete type that includes
`groupby.rows` and `groupby.columns` instead of `any`. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The predicate parameter is explicitly annotated as `any`, so the new code
violates the no-any-types rule.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=5bf4f19075b54fdeb80248f3f761ecb6&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=5bf4f19075b54fdeb80248f3f761ecb6&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
**Line:** 124:124
**Comment:**
*Custom Rule: Give the predicate parameter a concrete type that
includes `groupby.rows` and `groupby.columns` instead of `any`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=934cb46acd166ad49f40420add1879f7c851d101e20d8e7eff60c3f9abd51ff2&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=934cb46acd166ad49f40420add1879f7c851d101e20d8e7eff60c3f9abd51ff2&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts:
##########
@@ -0,0 +1,446 @@
+/*
+ * 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 { TimeGranularity } from '@superset-ui/core';
+import buildGroupbyCombinations, {
+ isAdditiveMetric,
+ allMetricsAdditive,
+ additiveReducerFor,
+ synthesizeAdditiveLevels,
+ splitGroupingSetsResult,
+ groupingMarkerLabel,
+} from '../../src/plugin/utilities';
+import { PivotTableQueryFormData, MetricsLayoutEnum } from '../../src/types';
+
+const baseFormData = {
+ groupbyRows: ['row1', 'row2'],
+ groupbyColumns: ['col1', 'col2'],
+ metrics: ['metric1', 'metric2'],
+ tableRenderer: 'Table With Subtotal',
+ colOrder: 'key_a_to_z',
+ rowOrder: 'key_a_to_z',
+ aggregateFunction: 'Sum',
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ transposePivot: false,
+ rowSubtotalPosition: true,
+ colSubtotalPosition: true,
+ colTotals: true,
+ colSubTotals: true,
+ rowTotals: true,
+ rowSubTotals: true,
+ valueFormat: 'SMART_NUMBER',
+ datasource: '5__table',
+ viz_type: 'my_chart',
+ width: 800,
+ height: 600,
+ combineMetric: false,
+ verboseMap: {},
+ columnFormats: {},
+ currencyFormats: {},
+ metricColorFormatters: [],
+ dateFormatters: {},
+ setDataMask: () => {},
+ legacy_order_by: 'count',
+ order_desc: true,
+ margin: 0,
+ time_grain_sqla: TimeGranularity.MONTH,
+ temporal_columns_lookup: { col1: true },
+ currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+} as unknown as PivotTableQueryFormData;
+
+test('should build all combinations for basic pivot table', () => {
+ const combinations = buildGroupbyCombinations(baseFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col1'] },
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: [] },
+ { rows: ['row1'], columns: ['col1'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(9);
+});
+
+test('should handle transposed pivot correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['row1'] },
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: [] },
+ { rows: ['col1'], columns: ['row1'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: [] },
+ { rows: ['col1', 'col2'], columns: ['row1'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should filter combinations when combineMetric is true with ROWS layout',
() => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should filter combinations when combineMetric is true with COLUMNS
layout', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should handle single dimension in rows only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['row'],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: ['row'], columns: [] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle single dimension in columns only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle empty groupby arrays', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([{ rows: [], columns: [] }]);
+
+ expect(combinations).toHaveLength(1);
+});
+
+test('should work with combineMetric and transposed pivot', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should handle combineMetric with empty arrays correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+});
+
+test('should work with large number of dimensions', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['r1', 'r2', 'r3', 'r4'],
+ groupbyColumns: ['c1', 'c2', 'c3'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toHaveLength(20);
+
+ expect(combinations).toContainEqual({ rows: [], columns: [] });
+ expect(combinations).toContainEqual({
+ rows: ['r1', 'r2', 'r3', 'r4'],
+ columns: ['c1', 'c2', 'c3'],
+ });
+});
+
+test('isAdditiveMetric: SIMPLE metrics with additive aggregates are additive',
() => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'num' },
+ label: 'sum_num',
+ } as any),
Review Comment:
**Suggestion:** Replace this `any` cast with the specific metric type used
by `isAdditiveMetric` so the test remains type-safe. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The file is new TypeScript test code and this call site uses `as any`, which
directly violates the no-any-types rule.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=9254d1f412f34e57b5f66c54a32442e1&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=9254d1f412f34e57b5f66c54a32442e1&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts
**Line:** 242:249
**Comment:**
*Custom Rule: Replace this `any` cast with the specific metric type
used by `isAdditiveMetric` so the test remains type-safe.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=17c59b68dddffbef8cdea54c2b769887e8038fc2868fcae01d3cadd8f64350aa&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=17c59b68dddffbef8cdea54c2b769887e8038fc2868fcae01d3cadd8f64350aa&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts:
##########
@@ -64,36 +63,73 @@ const chartProps = new ChartProps<QueryFormData>({
theme: supersetTheme,
});
-test('should transform chart props for viz', () => {
- expect(transformProps(chartProps)).toEqual({
- width: 800,
- height: 600,
- groupbyRows: ['row1', 'row2'],
- groupbyColumns: ['col1', 'col2'],
- metrics: ['metric1', 'metric2'],
- tableRenderer: 'Table With Subtotal',
- colOrder: 'key_a_to_z',
- rowOrder: 'key_a_to_z',
- aggregateFunction: 'Sum',
- transposePivot: true,
- combineMetric: true,
- rowSubtotalPosition: true,
- colSubtotalPosition: true,
+test('should pass through formData props for viz', () => {
+ const result = transformProps(chartProps) as any;
+ expect(result.width).toBe(800);
+ expect(result.height).toBe(600);
+ expect(result.groupbyRows).toEqual(['row1', 'row2']);
+ expect(result.groupbyColumns).toEqual(['col1', 'col2']);
+ expect(result.metrics).toEqual(['metric1', 'metric2']);
+ expect(result.metricsLayout).toBe(MetricsLayoutEnum.COLUMNS);
+ expect(result.currencyFormat).toEqual({
+ symbol: 'USD',
+ symbolPosition: 'prefix',
+ });
+ // data is the per-level QueryData[] (split/synthesized), not raw rows.
+ expect(Array.isArray(result.data)).toBe(true);
+ result.data.forEach((level: any) => {
+ expect(level).toHaveProperty('groupby');
+ expect(level).toHaveProperty('data');
+ });
+});
+
+test('non-additive: transformProps splits the GROUPING SETS result by level',
() => {
+ const gm = (col: string) => `${col}__superset_grouping`;
+ const localFormData = {
+ ...formData,
+ combineMetric: false,
+ transposePivot: false,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ groupbyRows: ['region'],
+ groupbyColumns: [],
colTotals: true,
rowTotals: true,
- valueFormat: 'SMART_NUMBER',
- data: [{ name: 'Hulk', sum__num: 1, __timestamp: 599616000000 }],
- setDataMask,
- selectedFilters: {},
- verboseMap: {},
- metricsLayout: MetricsLayoutEnum.COLUMNS,
- metricColorFormatters: [],
- dateFormatters: {},
- emitCrossFilters: false,
- columnFormats: {},
- currencyFormats: {},
- currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+ metrics: ['m'], // saved-metric string -> non-additive
+ };
+ const cp = new ChartProps<QueryFormData>({
+ formData: localFormData as unknown as QueryFormData,
+ width: 800,
+ height: 600,
+ queriesData: [
+ {
+ // One combined GROUPING SETS result: leaf rows (region marker 0) +
+ // grand total row (region marker 1).
+ data: [
+ { region: 'US', m: 10, [gm('region')]: 0 },
+ { region: 'EU', m: 5, [gm('region')]: 0 },
+ { region: null, m: 15, [gm('region')]: 1 },
+ ],
+ colnames: ['region', 'm', gm('region')],
+ coltypes: [1, 0, 0],
+ },
+ ],
+ hooks: { setDataMask },
+ filterState: { selectedFilters: {} },
+ datasource: { verboseMap: {}, columnFormats: {} },
+ theme: supersetTheme,
});
+
+ const result = transformProps(cp) as any;
Review Comment:
**Suggestion:** Replace this `any` assertion with the expected typed
transform output for the non-additive case. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
This line uses `as any` in newly added test code, which matches the custom
ruleβs prohibited pattern exactly.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=36fb8714c26742b89470a454a250b472&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=36fb8714c26742b89470a454a250b472&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
**Line:** 122:122
**Comment:**
*Custom Rule: Replace this `any` assertion with the expected typed
transform output for the non-additive case.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=108b8cd0e029b9c189333801a874c09856f80f0c6384a9c0be0143275b53160d&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=108b8cd0e029b9c189333801a874c09856f80f0c6384a9c0be0143275b53160d&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts:
##########
@@ -0,0 +1,446 @@
+/*
+ * 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 { TimeGranularity } from '@superset-ui/core';
+import buildGroupbyCombinations, {
+ isAdditiveMetric,
+ allMetricsAdditive,
+ additiveReducerFor,
+ synthesizeAdditiveLevels,
+ splitGroupingSetsResult,
+ groupingMarkerLabel,
+} from '../../src/plugin/utilities';
+import { PivotTableQueryFormData, MetricsLayoutEnum } from '../../src/types';
+
+const baseFormData = {
+ groupbyRows: ['row1', 'row2'],
+ groupbyColumns: ['col1', 'col2'],
+ metrics: ['metric1', 'metric2'],
+ tableRenderer: 'Table With Subtotal',
+ colOrder: 'key_a_to_z',
+ rowOrder: 'key_a_to_z',
+ aggregateFunction: 'Sum',
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ transposePivot: false,
+ rowSubtotalPosition: true,
+ colSubtotalPosition: true,
+ colTotals: true,
+ colSubTotals: true,
+ rowTotals: true,
+ rowSubTotals: true,
+ valueFormat: 'SMART_NUMBER',
+ datasource: '5__table',
+ viz_type: 'my_chart',
+ width: 800,
+ height: 600,
+ combineMetric: false,
+ verboseMap: {},
+ columnFormats: {},
+ currencyFormats: {},
+ metricColorFormatters: [],
+ dateFormatters: {},
+ setDataMask: () => {},
+ legacy_order_by: 'count',
+ order_desc: true,
+ margin: 0,
+ time_grain_sqla: TimeGranularity.MONTH,
+ temporal_columns_lookup: { col1: true },
+ currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+} as unknown as PivotTableQueryFormData;
+
+test('should build all combinations for basic pivot table', () => {
+ const combinations = buildGroupbyCombinations(baseFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col1'] },
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: [] },
+ { rows: ['row1'], columns: ['col1'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(9);
+});
+
+test('should handle transposed pivot correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['row1'] },
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: [] },
+ { rows: ['col1'], columns: ['row1'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: [] },
+ { rows: ['col1', 'col2'], columns: ['row1'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should filter combinations when combineMetric is true with ROWS layout',
() => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should filter combinations when combineMetric is true with COLUMNS
layout', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should handle single dimension in rows only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['row'],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: ['row'], columns: [] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle single dimension in columns only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle empty groupby arrays', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([{ rows: [], columns: [] }]);
+
+ expect(combinations).toHaveLength(1);
+});
+
+test('should work with combineMetric and transposed pivot', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should handle combineMetric with empty arrays correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+});
+
+test('should work with large number of dimensions', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['r1', 'r2', 'r3', 'r4'],
+ groupbyColumns: ['c1', 'c2', 'c3'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toHaveLength(20);
+
+ expect(combinations).toContainEqual({ rows: [], columns: [] });
+ expect(combinations).toContainEqual({
+ rows: ['r1', 'r2', 'r3', 'r4'],
+ columns: ['c1', 'c2', 'c3'],
+ });
+});
+
+test('isAdditiveMetric: SIMPLE metrics with additive aggregates are additive',
() => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'num' },
+ label: 'sum_num',
+ } as any),
+ ).toBe(true);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT',
+ column: { column_name: 'num' },
+ label: 'count_num',
+ } as any),
+ ).toBe(true);
+});
+
+test('isAdditiveMetric: non-additive aggregates, SQL, and saved metrics are
not additive', () => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'AVG',
+ column: { column_name: 'num' },
+ label: 'avg_num',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT_DISTINCT',
+ column: { column_name: 'name' },
+ label: 'distinct_names',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SQL',
+ sqlExpression: 'SUM(a) / SUM(b)',
+ label: 'ratio',
+ } as any),
+ ).toBe(false);
+ // saved-metric reference: aggregate unknown from form data -> non-additive
+ expect(isAdditiveMetric('count' as any)).toBe(false);
+});
+
+test('allMetricsAdditive: all additive vs any non-additive vs empty', () => {
+ const sum = {
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'a' },
+ label: 'a',
+ } as any;
+ const ratio = {
+ expressionType: 'SQL',
+ sqlExpression: 'SUM(a)/SUM(b)',
+ label: 'r',
+ } as any;
+ expect(allMetricsAdditive([sum, sum])).toBe(true);
+ expect(allMetricsAdditive([sum, ratio])).toBe(false);
+ expect(allMetricsAdditive([])).toBe(false);
+});
+
+test('pruning: with all totals/subtotals off, only the leaf level is queried',
() => {
+ const combinations = buildGroupbyCombinations({
+ ...baseFormData,
+ colTotals: false,
+ rowTotals: false,
+ colSubTotals: false,
+ rowSubTotals: false,
+ });
+ expect(combinations).toEqual([
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+});
+
+test('pruning: totals on, subtotals off -> only full + fully-collapsed
prefixes', () => {
+ const combinations = buildGroupbyCombinations({
+ ...baseFormData,
+ colTotals: true,
+ rowTotals: true,
+ colSubTotals: false,
+ rowSubTotals: false,
+ });
+ // 2 row prefixes ([], full) x 2 col prefixes ([], full) = 4
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+});
+
+test('pruning: row subtotals on, everything else off', () => {
+ const combinations = buildGroupbyCombinations({
+ ...baseFormData,
+ colTotals: false,
+ rowTotals: false,
+ colSubTotals: false,
+ rowSubTotals: true,
+ });
+ // row prefixes: intermediate [row1] + full [row1,row2]; col prefixes: full
only
+ expect(combinations).toEqual([
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+});
+
+test('pruning: empty column dims keep the [] (leaf) level regardless of
rowTotals', () => {
+ const combinations = buildGroupbyCombinations({
+ ...baseFormData,
+ groupbyColumns: [],
+ colTotals: true, // bottom total row
+ rowTotals: false,
+ colSubTotals: false,
+ rowSubTotals: false,
+ });
+ // columns is empty so [] is the leaf level (always kept); rows: []
(colTotals) + full
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: ['row1', 'row2'], columns: [] },
+ ]);
+});
+
+test('additiveReducerFor maps aggregates to reducers', () => {
+ const mk = (aggregate: string) =>
+ ({ expressionType: 'SIMPLE', aggregate, label: aggregate }) as any;
+ expect(additiveReducerFor(mk('SUM'))).toBe('sum');
+ expect(additiveReducerFor(mk('COUNT'))).toBe('sum');
+ expect(additiveReducerFor(mk('MIN'))).toBe('min');
+ expect(additiveReducerFor(mk('MAX'))).toBe('max');
+ expect(additiveReducerFor('saved_metric' as any)).toBe('sum');
Review Comment:
**Suggestion:** Pass a correctly typed saved-metric value to
`additiveReducerFor` and remove the `any` cast. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The new code uses an explicit `any` cast in TypeScript, so this is a real
violation of the custom rule.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=7b14c04d4d62433689e868fd62595723&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=7b14c04d4d62433689e868fd62595723&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts
**Line:** 367:374
**Comment:**
*Custom Rule: Pass a correctly typed saved-metric value to
`additiveReducerFor` and remove the `any` cast.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=655e09c7e0870db465a61b4a8b8e15020b594e86782e5a19725388d452c54c11&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=655e09c7e0870db465a61b4a8b8e15020b594e86782e5a19725388d452c54c11&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts:
##########
@@ -0,0 +1,446 @@
+/*
+ * 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 { TimeGranularity } from '@superset-ui/core';
+import buildGroupbyCombinations, {
+ isAdditiveMetric,
+ allMetricsAdditive,
+ additiveReducerFor,
+ synthesizeAdditiveLevels,
+ splitGroupingSetsResult,
+ groupingMarkerLabel,
+} from '../../src/plugin/utilities';
+import { PivotTableQueryFormData, MetricsLayoutEnum } from '../../src/types';
+
+const baseFormData = {
+ groupbyRows: ['row1', 'row2'],
+ groupbyColumns: ['col1', 'col2'],
+ metrics: ['metric1', 'metric2'],
+ tableRenderer: 'Table With Subtotal',
+ colOrder: 'key_a_to_z',
+ rowOrder: 'key_a_to_z',
+ aggregateFunction: 'Sum',
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ transposePivot: false,
+ rowSubtotalPosition: true,
+ colSubtotalPosition: true,
+ colTotals: true,
+ colSubTotals: true,
+ rowTotals: true,
+ rowSubTotals: true,
+ valueFormat: 'SMART_NUMBER',
+ datasource: '5__table',
+ viz_type: 'my_chart',
+ width: 800,
+ height: 600,
+ combineMetric: false,
+ verboseMap: {},
+ columnFormats: {},
+ currencyFormats: {},
+ metricColorFormatters: [],
+ dateFormatters: {},
+ setDataMask: () => {},
+ legacy_order_by: 'count',
+ order_desc: true,
+ margin: 0,
+ time_grain_sqla: TimeGranularity.MONTH,
+ temporal_columns_lookup: { col1: true },
+ currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+} as unknown as PivotTableQueryFormData;
+
+test('should build all combinations for basic pivot table', () => {
+ const combinations = buildGroupbyCombinations(baseFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col1'] },
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: [] },
+ { rows: ['row1'], columns: ['col1'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(9);
+});
+
+test('should handle transposed pivot correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['row1'] },
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: [] },
+ { rows: ['col1'], columns: ['row1'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: [] },
+ { rows: ['col1', 'col2'], columns: ['row1'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should filter combinations when combineMetric is true with ROWS layout',
() => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should filter combinations when combineMetric is true with COLUMNS
layout', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should handle single dimension in rows only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['row'],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: ['row'], columns: [] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle single dimension in columns only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle empty groupby arrays', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([{ rows: [], columns: [] }]);
+
+ expect(combinations).toHaveLength(1);
+});
+
+test('should work with combineMetric and transposed pivot', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should handle combineMetric with empty arrays correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+});
+
+test('should work with large number of dimensions', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['r1', 'r2', 'r3', 'r4'],
+ groupbyColumns: ['c1', 'c2', 'c3'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toHaveLength(20);
+
+ expect(combinations).toContainEqual({ rows: [], columns: [] });
+ expect(combinations).toContainEqual({
+ rows: ['r1', 'r2', 'r3', 'r4'],
+ columns: ['c1', 'c2', 'c3'],
+ });
+});
+
+test('isAdditiveMetric: SIMPLE metrics with additive aggregates are additive',
() => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'num' },
+ label: 'sum_num',
+ } as any),
+ ).toBe(true);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT',
+ column: { column_name: 'num' },
+ label: 'count_num',
+ } as any),
+ ).toBe(true);
+});
+
+test('isAdditiveMetric: non-additive aggregates, SQL, and saved metrics are
not additive', () => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'AVG',
+ column: { column_name: 'num' },
+ label: 'avg_num',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT_DISTINCT',
+ column: { column_name: 'name' },
+ label: 'distinct_names',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SQL',
+ sqlExpression: 'SUM(a) / SUM(b)',
+ label: 'ratio',
+ } as any),
+ ).toBe(false);
+ // saved-metric reference: aggregate unknown from form data -> non-additive
+ expect(isAdditiveMetric('count' as any)).toBe(false);
Review Comment:
**Suggestion:** Remove the `any` cast and type this saved-metric argument
with the correct union/member type accepted by `isAdditiveMetric`. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
This new test code contains an explicit `any` cast, so it matches the rule
forbidding `any` in changed TypeScript code.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=094fe6a79d264f23ba45025a6e9737a7&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=094fe6a79d264f23ba45025a6e9737a7&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts
**Line:** 286:286
**Comment:**
*Custom Rule: Remove the `any` cast and type this saved-metric argument
with the correct union/member type accepted by `isAdditiveMetric`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=12a741a48e9e04ca7116848399d636959db15e62d7d0005184d2d532128286e6&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=12a741a48e9e04ca7116848399d636959db15e62d7d0005184d2d532128286e6&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts:
##########
@@ -335,3 +371,58 @@ test('should map conditional formatting rules to
metricColorFormatters with corr
result.metricColorFormatters[1].getColorFromValue(column2Formatting),
).toEqual('#5ac189FF');
});
+
+test('additive metrics: synthesizes rollup levels from a single leaf query',
() => {
+ const additiveFormData = {
+ ...formData,
+ combineMetric: false,
+ transposePivot: false,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ groupbyRows: ['region'],
+ groupbyColumns: [],
+ colTotals: true,
+ rowTotals: true,
+ metrics: [
+ {
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'v' },
+ label: 'v',
+ },
+ ],
+ };
+ const additiveChartProps = new ChartProps<QueryFormData>({
+ formData: additiveFormData as unknown as QueryFormData,
+ width: 800,
+ height: 600,
+ queriesData: [
+ {
+ data: [
+ { region: 'US', v: 10 },
+ { region: 'EU', v: 5 },
+ ],
+ colnames: ['region', 'v'],
+ coltypes: [1, 0],
+ },
+ ],
+ hooks: { setDataMask },
+ filterState: { selectedFilters: {} },
+ datasource: { verboseMap: {}, columnFormats: {} },
+ theme: supersetTheme,
+ });
+
+ const result = transformProps(additiveChartProps);
+ // One query produced multiple synthesized rollup levels.
+ expect(result.data.length).toBeGreaterThan(1);
+ // Grand-total level: region collapsed -> v = 10 + 5 = 15.
+ const grand = (result.data as any[]).find(
Review Comment:
**Suggestion:** Replace the `any[]` cast with a concrete array element type
for rollup level entries. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The code casts `result.data` to `any[]`, which is precisely the type usage
the rule forbids in changed TypeScript code.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=d9734be3825d428aae92c67ac088858f&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=d9734be3825d428aae92c67ac088858f&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/transformProps.test.ts
**Line:** 418:418
**Comment:**
*Custom Rule: Replace the `any[]` cast with a concrete array element
type for rollup level entries.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=48b79180b7b6d09cdfcf9fd73cdfe520359fc6ff506dd0a06974c1a86f14d561&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=48b79180b7b6d09cdfcf9fd73cdfe520359fc6ff506dd0a06974c1a86f14d561&reaction=dislike'>π</a>
##########
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts:
##########
@@ -0,0 +1,446 @@
+/*
+ * 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 { TimeGranularity } from '@superset-ui/core';
+import buildGroupbyCombinations, {
+ isAdditiveMetric,
+ allMetricsAdditive,
+ additiveReducerFor,
+ synthesizeAdditiveLevels,
+ splitGroupingSetsResult,
+ groupingMarkerLabel,
+} from '../../src/plugin/utilities';
+import { PivotTableQueryFormData, MetricsLayoutEnum } from '../../src/types';
+
+const baseFormData = {
+ groupbyRows: ['row1', 'row2'],
+ groupbyColumns: ['col1', 'col2'],
+ metrics: ['metric1', 'metric2'],
+ tableRenderer: 'Table With Subtotal',
+ colOrder: 'key_a_to_z',
+ rowOrder: 'key_a_to_z',
+ aggregateFunction: 'Sum',
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ transposePivot: false,
+ rowSubtotalPosition: true,
+ colSubtotalPosition: true,
+ colTotals: true,
+ colSubTotals: true,
+ rowTotals: true,
+ rowSubTotals: true,
+ valueFormat: 'SMART_NUMBER',
+ datasource: '5__table',
+ viz_type: 'my_chart',
+ width: 800,
+ height: 600,
+ combineMetric: false,
+ verboseMap: {},
+ columnFormats: {},
+ currencyFormats: {},
+ metricColorFormatters: [],
+ dateFormatters: {},
+ setDataMask: () => {},
+ legacy_order_by: 'count',
+ order_desc: true,
+ margin: 0,
+ time_grain_sqla: TimeGranularity.MONTH,
+ temporal_columns_lookup: { col1: true },
+ currencyFormat: { symbol: 'USD', symbolPosition: 'prefix' },
+} as unknown as PivotTableQueryFormData;
+
+test('should build all combinations for basic pivot table', () => {
+ const combinations = buildGroupbyCombinations(baseFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col1'] },
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: [] },
+ { rows: ['row1'], columns: ['col1'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(9);
+});
+
+test('should handle transposed pivot correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['row1'] },
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: [] },
+ { rows: ['col1'], columns: ['row1'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: [] },
+ { rows: ['col1', 'col2'], columns: ['row1'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should filter combinations when combineMetric is true with ROWS layout',
() => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: ['row1', 'row2'], columns: [] },
+ { rows: ['row1', 'row2'], columns: ['col1'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should filter combinations when combineMetric is true with COLUMNS
layout', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['col1', 'col2'] },
+ { rows: ['row1'], columns: ['col1', 'col2'] },
+ { rows: ['row1', 'row2'], columns: ['col1', 'col2'] },
+ ]);
+
+ expect(combinations).toHaveLength(3);
+});
+
+test('should handle single dimension in rows only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['row'],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: ['row'], columns: [] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle single dimension in columns only', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+
+ expect(combinations).toHaveLength(2);
+});
+
+test('should handle empty groupby arrays', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: [],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([{ rows: [], columns: [] }]);
+
+ expect(combinations).toHaveLength(1);
+});
+
+test('should work with combineMetric and transposed pivot', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ transposePivot: true,
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.COLUMNS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: ['row1', 'row2'] },
+ { rows: ['col1'], columns: ['row1', 'row2'] },
+ { rows: ['col1', 'col2'], columns: ['row1', 'row2'] },
+ ]);
+});
+
+test('should handle combineMetric with empty arrays correctly', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: [],
+ groupbyColumns: ['col'],
+ combineMetric: true,
+ metricsLayout: MetricsLayoutEnum.ROWS,
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toEqual([
+ { rows: [], columns: [] },
+ { rows: [], columns: ['col'] },
+ ]);
+});
+
+test('should work with large number of dimensions', () => {
+ const modifiedFormData = {
+ ...baseFormData,
+ groupbyRows: ['r1', 'r2', 'r3', 'r4'],
+ groupbyColumns: ['c1', 'c2', 'c3'],
+ };
+
+ const combinations = buildGroupbyCombinations(modifiedFormData);
+
+ expect(combinations).toHaveLength(20);
+
+ expect(combinations).toContainEqual({ rows: [], columns: [] });
+ expect(combinations).toContainEqual({
+ rows: ['r1', 'r2', 'r3', 'r4'],
+ columns: ['c1', 'c2', 'c3'],
+ });
+});
+
+test('isAdditiveMetric: SIMPLE metrics with additive aggregates are additive',
() => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'num' },
+ label: 'sum_num',
+ } as any),
+ ).toBe(true);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT',
+ column: { column_name: 'num' },
+ label: 'count_num',
+ } as any),
+ ).toBe(true);
+});
+
+test('isAdditiveMetric: non-additive aggregates, SQL, and saved metrics are
not additive', () => {
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'AVG',
+ column: { column_name: 'num' },
+ label: 'avg_num',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SIMPLE',
+ aggregate: 'COUNT_DISTINCT',
+ column: { column_name: 'name' },
+ label: 'distinct_names',
+ } as any),
+ ).toBe(false);
+ expect(
+ isAdditiveMetric({
+ expressionType: 'SQL',
+ sqlExpression: 'SUM(a) / SUM(b)',
+ label: 'ratio',
+ } as any),
+ ).toBe(false);
+ // saved-metric reference: aggregate unknown from form data -> non-additive
+ expect(isAdditiveMetric('count' as any)).toBe(false);
+});
+
+test('allMetricsAdditive: all additive vs any non-additive vs empty', () => {
+ const sum = {
+ expressionType: 'SIMPLE',
+ aggregate: 'SUM',
+ column: { column_name: 'a' },
+ label: 'a',
+ } as any;
Review Comment:
**Suggestion:** Define `sum` with an explicit metric interface/type instead
of using `as any`. [custom_rule]
**Severity Level:** Minor β οΈ
<details>
<summary><b>Why it matters? π€ </b></summary>
The `sum` fixture is introduced in new TypeScript code and uses `as any`,
which is a direct violation of the custom rule.
</details>
<details>
<summary><b>Rule source π </b></summary>
.cursor/rules/dev-standard.mdc (line 16)
</details>
[](https://app.codeant.ai/fix-in-ide?tool=cursor&prompt_id=5bebaa441f404cb495bfb9ee7129c1e3&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
[](https://app.codeant.ai/fix-in-ide?tool=vscode-claude&prompt_id=5bebaa441f404cb495bfb9ee7129c1e3&service=github&base_url=https%3A%2F%2Fgithub.com&org=apache&repo=apache%2Fsuperset)
*(Use Cmd/Ctrl + Click for best experience)*
<details>
<summary><b>Prompt for AI Agent π€ </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset-frontend/plugins/plugin-chart-pivot-table/test/plugin/utilities.test.ts
**Line:** 289:295
**Comment:**
*Custom Rule: Define `sum` with an explicit metric interface/type
instead of using `as any`.
Validate the correctness of the flagged issue. If correct, How can I resolve
this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask
user if the user wants to fix the rest of the comments as well. if said yes,
then fetch all the comments validate the correctness and implement a minimal fix
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=73e679571912b09af091ebac7ae9e8b98579ad1b293cdce73a521989b1566aba&reaction=like'>π</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F41184&comment_hash=73e679571912b09af091ebac7ae9e8b98579ad1b293cdce73a521989b1566aba&reaction=dislike'>π</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]