diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/styles.css b/datajunction-ui/src/app/pages/AddEditNodePage/styles.css
index 72f274491..1eb0b846c 100644
--- a/datajunction-ui/src/app/pages/AddEditNodePage/styles.css
+++ b/datajunction-ui/src/app/pages/AddEditNodePage/styles.css
@@ -139,11 +139,11 @@
margin: 0 !important;
}
-.node-builder .SemiAdditiveFields {
+.node-builder .ReaggregateFields {
gap: 12px;
}
-.node-builder .SemiAdditiveFields > * {
+.node-builder .ReaggregateFields > * {
flex: 1;
min-width: 0;
margin: 0 !important;
diff --git a/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx b/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx
index 86e6fc589..bf5667944 100644
--- a/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx
+++ b/datajunction-ui/src/app/pages/NodePage/NodeInfoTab.jsx
@@ -22,9 +22,11 @@ const dimensionNodePath = dimension => {
return parts.slice(0, -1).join('.');
};
-const semiAdditiveFunctionLabel = func =>
+const reaggregateFunctionLabel = func =>
func ? labelize(func.toLowerCase()) : null;
+const firstReaggregateRule = reaggregate => reaggregate?.rules?.[0];
+
// interface MetricInfo {
// name: string;
// current: MetricRevision;
@@ -74,7 +76,7 @@ export default function NodeInfoTab({ node }) {
expression: metric.current.metricMetadata?.expression,
incompatible_druid_functions:
metric.current.metricMetadata?.incompatibleDruidFunctions || [],
- semi_additive: metric.current.semiAdditive,
+ reaggregate: metric.current.reaggregate,
});
};
if (node.type === 'metric') {
@@ -289,22 +291,26 @@ export default function NodeInfoTab({ node }) {
className="mb-0 opacity-75"
role="dialog"
aria-hidden="false"
- aria-label="SemiAdditive"
+ aria-label="Reaggregate"
>
- {metricInfo?.semi_additive ? (
+ {firstReaggregateRule(metricInfo?.reaggregate) ? (
<>
- {semiAdditiveFunctionLabel(metricInfo.semi_additive.function)}
+ {reaggregateFunctionLabel(
+ firstReaggregateRule(metricInfo.reaggregate).fn,
+ )}
{' on '}
- {dimensionNodePath(metricInfo.semi_additive.dimension) ? (
+ {dimensionNodePath(
+ firstReaggregateRule(metricInfo.reaggregate).dimension,
+ ) ? (
) : (
- metricInfo.semi_additive.dimension
+ firstReaggregateRule(metricInfo.reaggregate).dimension
)}
>
) : (
diff --git a/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
index 349c2fdbf..66e676b95 100644
--- a/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
+++ b/datajunction-ui/src/app/pages/NodePage/__tests__/NodePage.test.jsx
@@ -389,7 +389,7 @@ describe('
', () => {
).toHaveTextContent('');
expect(
- screen.getByRole('dialog', { name: 'SemiAdditive' }),
+ screen.getByRole('dialog', { name: 'Reaggregate' }),
).toHaveTextContent('None');
expect(
@@ -425,9 +425,13 @@ describe('
', () => {
...mocks.mockMetricNodeJson,
current: {
...mocks.mockMetricNodeJson.current,
- semiAdditive: {
- dimension: 'v3.date.date_id[order]',
- function: 'LAST_VALUE',
+ reaggregate: {
+ rules: [
+ {
+ dimension: 'v3.date.date_id[order]',
+ fn: 'LAST_VALUE',
+ },
+ ],
},
},
});
@@ -444,11 +448,11 @@ describe('
', () => {
,
);
- const semiAdditive = await screen.findByRole('dialog', {
- name: 'SemiAdditive',
+ const reaggregate = await screen.findByRole('dialog', {
+ name: 'Reaggregate',
});
await waitFor(() => {
- expect(semiAdditive).toHaveTextContent(
+ expect(reaggregate).toHaveTextContent(
'Last Value on v3.date.date_id[order]',
);
});
diff --git a/datajunction-ui/src/app/services/DJService.js b/datajunction-ui/src/app/services/DJService.js
index c8862c8d2..ed3ac4399 100644
--- a/datajunction-ui/src/app/services/DJService.js
+++ b/datajunction-ui/src/app/services/DJService.js
@@ -520,9 +520,13 @@ export const DataJunctionAPI = {
requiredDimensions {
name
}
- semiAdditive {
- dimension
- function
+ reaggregate {
+ fn
+ weight
+ rules {
+ dimension
+ fn
+ }
}
mode
customMetadata
@@ -612,9 +616,13 @@ export const DataJunctionAPI = {
requiredDimensions {
name
}
- semiAdditive {
- dimension
- function
+ reaggregate {
+ fn
+ weight
+ rules {
+ dimension
+ fn
+ }
}
}
}
@@ -755,7 +763,7 @@ export const DataJunctionAPI = {
metric_unit,
required_dimensions,
custom_metadata,
- semi_additive,
+ reaggregate,
) {
const metricMetadata =
metric_direction || metric_unit
@@ -776,7 +784,7 @@ export const DataJunctionAPI = {
metric_metadata: metricMetadata,
required_dimensions: required_dimensions,
custom_metadata: custom_metadata,
- semi_additive: semi_additive,
+ reaggregate: reaggregate,
};
// Remove undefined fields to avoid sending them to the API
Object.keys(requestBody).forEach(
@@ -807,7 +815,7 @@ export const DataJunctionAPI = {
required_dimensions,
owners,
custom_metadata,
- semi_additive,
+ reaggregate,
) {
try {
const metricMetadata =
@@ -829,7 +837,7 @@ export const DataJunctionAPI = {
required_dimensions: required_dimensions,
owners: owners,
custom_metadata: custom_metadata,
- semi_additive: semi_additive,
+ reaggregate: reaggregate,
};
// Remove undefined fields to avoid sending them to the API
Object.keys(requestBody).forEach(
diff --git a/datajunction-ui/src/app/services/__tests__/DJService.test.jsx b/datajunction-ui/src/app/services/__tests__/DJService.test.jsx
index fe21e5da0..5d022bbf5 100644
--- a/datajunction-ui/src/app/services/__tests__/DJService.test.jsx
+++ b/datajunction-ui/src/app/services/__tests__/DJService.test.jsx
@@ -198,8 +198,12 @@ describe('DataJunctionAPI', () => {
undefined,
null,
{
- dimension: 'v3.date.date_id[order]',
- function: 'last_value',
+ rules: [
+ {
+ dimension: 'v3.date.date_id[order]',
+ fn: 'last_value',
+ },
+ ],
},
);
expect(fetch).toHaveBeenCalledWith(`${DJ_URL}/nodes/metric`, {
@@ -218,9 +222,13 @@ describe('DataJunctionAPI', () => {
metric_metadata: null,
required_dimensions: undefined,
custom_metadata: null,
- semi_additive: {
- dimension: 'v3.date.date_id[order]',
- function: 'last_value',
+ reaggregate: {
+ rules: [
+ {
+ dimension: 'v3.date.date_id[order]',
+ fn: 'last_value',
+ },
+ ],
},
}),
credentials: 'include',
@@ -294,8 +302,12 @@ describe('DataJunctionAPI', () => {
['dj'],
null,
{
- dimension: 'v3.date.date_id[order]',
- function: 'last_value',
+ rules: [
+ {
+ dimension: 'v3.date.date_id[order]',
+ fn: 'last_value',
+ },
+ ],
},
);
expect(fetch).toHaveBeenCalledWith(
@@ -319,9 +331,13 @@ describe('DataJunctionAPI', () => {
required_dimensions: [],
owners: ['dj'],
custom_metadata: null,
- semi_additive: {
- dimension: 'v3.date.date_id[order]',
- function: 'last_value',
+ reaggregate: {
+ rules: [
+ {
+ dimension: 'v3.date.date_id[order]',
+ fn: 'last_value',
+ },
+ ],
},
}),
credentials: 'include',
@@ -1375,9 +1391,10 @@ describe('DataJunctionAPI', () => {
expect(requestBody.variables).toEqual({
name: 'default.num_repair_orders',
});
- expect(requestBody.query).toContain('semiAdditive');
+ expect(requestBody.query).toContain('reaggregate');
expect(requestBody.query).toContain('dimension');
- expect(requestBody.query).toContain('function');
+ expect(requestBody.query).toContain('rules');
+ expect(requestBody.query).toContain('fn');
});
it('calls notebookExportCube correctly', async () => {
@@ -1998,9 +2015,10 @@ describe('DataJunctionAPI', () => {
const requestBody = JSON.parse(fetch.mock.calls[0][1].body);
expect(result).toHaveProperty('name', 'default.node1');
expect(requestBody.variables).toEqual({ name: 'default.node1' });
- expect(requestBody.query).toContain('semiAdditive');
+ expect(requestBody.query).toContain('reaggregate');
expect(requestBody.query).toContain('dimension');
- expect(requestBody.query).toContain('function');
+ expect(requestBody.query).toContain('rules');
+ expect(requestBody.query).toContain('fn');
});
it('returns null when getNodeForEditing finds no nodes', async () => {
From 8764f1bbc1e01e64c9606f38ce2ee69ad1ba487c Mon Sep 17 00:00:00 2001
From: Beto Dealmeida
Date: Thu, 3 Sep 2026 12:05:05 -0400
Subject: [PATCH 13/24] Prepare semi-additive metrics for review
---
...000-rg0001reaggregate_add_reaggregate_to_noderevision.py | 4 ++--
.../src/app/pages/AddEditNodePage/ReaggregateFields.jsx | 6 +-----
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/datajunction-server/datajunction_server/alembic/versions/2026_08_24_0000-rg0001reaggregate_add_reaggregate_to_noderevision.py b/datajunction-server/datajunction_server/alembic/versions/2026_08_24_0000-rg0001reaggregate_add_reaggregate_to_noderevision.py
index 663c46590..8a04b3fd2 100644
--- a/datajunction-server/datajunction_server/alembic/versions/2026_08_24_0000-rg0001reaggregate_add_reaggregate_to_noderevision.py
+++ b/datajunction-server/datajunction_server/alembic/versions/2026_08_24_0000-rg0001reaggregate_add_reaggregate_to_noderevision.py
@@ -2,7 +2,7 @@
Add reaggregate column to noderevision
Revision ID: rg0001reaggregate
-Revises: nsboundaryflag1
+Revises: cm0003dropowner
Create Date: 2026-08-24 00:00:00.000000+00:00
"""
@@ -11,7 +11,7 @@
# revision identifiers, used by Alembic.
revision = "rg0001reaggregate"
-down_revision = "nsboundaryflag1"
+down_revision = "cm0003dropowner"
branch_labels = None
depends_on = None
diff --git a/datajunction-ui/src/app/pages/AddEditNodePage/ReaggregateFields.jsx b/datajunction-ui/src/app/pages/AddEditNodePage/ReaggregateFields.jsx
index 7a8535f67..d163361b9 100644
--- a/datajunction-ui/src/app/pages/AddEditNodePage/ReaggregateFields.jsx
+++ b/datajunction-ui/src/app/pages/AddEditNodePage/ReaggregateFields.jsx
@@ -65,11 +65,7 @@ export const ReaggregateFields = () => {
-
+
{SEMI_ADDITIVE_FUNCTIONS.map(func => (