fix(studio): sum numeric-string columns in cumulative SQL charts (#47378)

Fixes: #47377

## What is the current behavior?

Enabling **Cumulative** on a results chart concatenates Y-axis values
instead
of summing them whenever the column is a `bigint`, `numeric`, `money`,
or
`count(*)` aggregate — which Postgres returns as JSON strings. For
per-row
values `10, 20, 30` the chart plots `10, 1020, 102030`.

`getCumulativeResults` ran `(prev[yKey] || 0) + row[yKey]` on raw result
rows.
The Y-axis selector explicitly allows numeric-string columns, so this is
a
common, fully-supported path (e.g. any `count(*) ... group by`).

## What is the new behavior?

Both operands are coerced with `Number()` before the addition, keeping
the
existing `|| 0` fallback for null/undefined/non-numeric values. The
series now
sums correctly: `10, 30, 60`.

The cumulative logic was previously duplicated in `ChartConfig.tsx` and
`QueryBlock.utils.ts` (which is how this bug slipped in twice). It is
now a
single shared, tested helper: `getCumulativeResults` lives in
`QueryBlock.utils.ts`, and `ChartConfig.tsx` imports it instead of
re-declaring
its own copy. The shared helper's `ChartConfig` type import is `import
type` to
avoid a runtime circular dependency, and its signature accepts
`readonly` rows
so both call sites type-check.

## Additional context

- Added regression tests for numeric-string inputs and for
null/undefined/non-numeric fallback to `0`. The existing tests only
covered
literal `number` inputs, never the string form Postgres actually
returns.
- Verified the new tests fail against the old code (`y: '010'`,
`'05undefined'`)
and pass with the fix. Full `QueryBlock.utils.test.ts` suite: 18
passing.

No migrations, no API changes, no infra changes.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed cumulative chart calculations so numeric values are always added
correctly, even when results arrive as strings.
* Improved handling of empty or non-numeric values in cumulative totals
so they are treated as zero instead of breaking the sum.

* **Tests**
* Added coverage for cumulative result calculations with numeric strings
and missing values.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Seid Muhammed authored and GitHub committed 2026-06-29 14:53:24 +00:00
1 parent 3674f173a3
commit affdcb35ff
3 files changed
+55 -18

No files matched your search

@@ -25,6 +25,7 @@ import { Admonition } from 'ui-patterns'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import BarChart from '@/components/ui/Charts/BarChart'
import NoDataPlaceholder from '@/components/ui/Charts/NoDataPlaceholder'
import { getCumulativeResults } from '@/components/ui/QueryBlock/QueryBlock.utils'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
type Results = { rows: readonly any[] }
@@ -40,21 +41,6 @@ export type ChartConfig = {
logScale?: boolean
}
const getCumulativeResults = (results: Results, config: ChartConfig) => {
if (!results?.rows?.length) {
return []
}
const cumulativeResults = results.rows.reduce((acc, row) => {
const prev = acc[acc.length - 1] || {}
const next = {
...row,
[config.yKey]: (prev[config.yKey] || 0) + row[config.yKey],
}
return [...acc, next]
}, [])
return cumulativeResults
}
const VALID_RESULT_KEY_TYPES = ['number', 'string', 'date']
type ChartConfigProps = {
@@ -1,4 +1,4 @@
import { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
import type { ChartConfig } from '@/components/interfaces/SQLEditor/UtilityPanel/ChartConfig'
export const checkHasNonPositiveValues = (data: Record<string, unknown>[], key: string): boolean =>
data.some((row) => (row[key] as number) <= 0)
@@ -47,16 +47,19 @@ export const formatLogTick = (value: number): string => {
return value.toLocaleString()
}
export const getCumulativeResults = (results: { rows: any[] }, config: ChartConfig) => {
export const getCumulativeResults = (results: { rows: readonly any[] }, config: ChartConfig) => {
if (!results?.rows?.length) {
return []
}
const cumulativeResults = results.rows.reduce((acc, row) => {
const prev = acc[acc.length - 1] || {}
// Coerce to Number before adding: Postgres returns `bigint`, `numeric`,
// `money` and `count(*)` columns as strings, so a bare `+` would
// concatenate (e.g. "10" + "20" -> "1020") instead of summing.
const next = {
...row,
[config.yKey]: (prev[config.yKey] || 0) + row[config.yKey],
[config.yKey]: (Number(prev[config.yKey]) || 0) + (Number(row[config.yKey]) || 0),
}
return [...acc, next]
}, [])
@@ -156,4 +156,52 @@ describe('getCumulativeResults', () => {
const output = getCumulativeResults(results, config)
expect(output).toEqual([{ x: 'a', y: 42 }])
})
// Postgres returns `bigint`, `numeric`, `money` and `count(*)` columns as
// strings, so the running total must sum numerically rather than concatenate.
it('sums numeric string yKey values instead of concatenating them', () => {
const results = {
rows: [
{ x: 'a', y: '10' },
{ x: 'b', y: '20' },
{ x: 'c', y: '30' },
],
}
const config = {
type: 'bar' as const,
xKey: 'x',
yKey: 'y',
cumulative: true,
showLabels: false,
showGrid: false,
}
const output = getCumulativeResults(results, config)
expect(output).toEqual([
{ x: 'a', y: 10 },
{ x: 'b', y: 30 },
{ x: 'c', y: 60 },
])
})
it('treats null/undefined and non-numeric yKey values as 0', () => {
const results = {
rows: [
{ x: 'a', y: null },
{ x: 'b', y: '5' },
{ x: 'c', y: undefined },
{ x: 'd', y: 'not-a-number' },
{ x: 'e', y: 4 },
],
}
const config = {
type: 'bar' as const,
xKey: 'x',
yKey: 'y',
cumulative: true,
showLabels: false,
showGrid: false,
}
const output = getCumulativeResults(results, config)
expect(output.map((row: { y: number }) => row.y)).toEqual([0, 5, 5, 5, 9])
})
})