Skip to content

fix: propagate historical body measurement changes - #1966

Merged
Asherlc merged 5 commits into
mainfrom
codex/issue-1769
Jul 26, 2026
Merged

Asherlc merged 5 commits into
mainfrom
codex/issue-1769

Conversation

@Asherlc

@Asherlc Asherlc commented Jul 25, 2026 •

Copy link
Copy Markdown
Owner

Fixes #1769

Summary

  • drive body measurement refreshes from per-user CDC change watermarks after the upstream refreshable view succeeds
  • emit serving-model tombstones for corrected or deleted historical measurement IDs
  • rematerialize dependent healthspan weeks and exclude deleted rows from server queries
  • migrate the existing ClickHouse table with lifecycle and watermark columns

Validation

  • real ClickHouse lifecycle integration: update, delete, tombstone, downstream exclusion passed
  • focused unit suite: 61 passed
  • pnpm typecheck
  • pnpm lint
  • dbt compile for daily_body_measurement weekly_healthspan

Local infrastructure note

The issue-specific ClickHouse test passed. A broader cycling fixture was interrupted by a concurrent local ClickHouse container restart under Docker VM pressure; no product/workflow workaround was added, and the evidence is recorded in the production incident baseline. CI remains the broad integration gate.


Summary by cubic

Propagates historical body measurement edits and deletes into the serving model and recomputes only the affected weeks. Fixes #1769 by bounding per-user refreshes with CDC watermarks capped by the body view’s last success time, and by emitting tombstones to prevent stale data.

  • Bug Fixes

    • Rebuilds daily_body_measurement incrementally per user using _peerdb_synced_at watermarks from analytics.body_measurement_sample, capped by system.view_refreshes.last_success_time (removes the prior 7-day overlap).
    • Emits tombstones for removed measurements; all downstream models and server queries exclude them (is_deleted = 0).
    • weekly_healthspan filters deleted rows and rematerializes only weeks touched since its last refresh using daily_body_measurement.refreshed_at.
  • Migration

    • Run ClickHouse migration 0056_daily_body_measurement_lifecycle to add is_deleted, source_synced_at, and a materialized minmax index on refreshed_at.
    • Guarded for fresh environments: skips if the serving table doesn’t exist yet; dbt creates the table with the final schema.

Written for commit 7c38ddb. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added lifecycle tracking for daily body measurements, including deletion status and source synchronization timestamps.
    • Improved incremental updates to detect refreshed measurements and propagate deletions to downstream health and weight analytics.
    • Added migration support for existing measurement tables.
  • Bug Fixes

    • Excluded deleted body measurements from weight charts and performance calculations.
    • Ensured weekly health metrics ignore deleted measurements.
  • Tests

    • Added coverage for measurement updates, deletions, migrations, and downstream reporting behavior.

Copilot AI review requested due to automatic review settings July 25, 2026 22:16
@cursor

cursor Bot commented Jul 25, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @Asherlc, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The daily body measurement model now uses source refresh watermarks, emits tombstones for removed measurements, and exposes lifecycle metadata. Weekly healthspan and server queries exclude deleted rows. A ClickHouse migration, integration coverage, and SQL assertions validate the lifecycle.

Changes

Body measurement lifecycle

Layer / File(s) Summary
Incremental daily model and tombstones
analytics/models/read_models/daily_body_measurement.sql
Incremental selection now uses view and source synchronization watermarks, preserves impacted keys, emits is_deleted rows, and records UTC source_synced_at metadata.
Weekly healthspan propagation
analytics/models/read_models/weekly_healthspan.sql, analytics/models/read_models/read_model_microbatch.sql.test.ts
Weekly healthspan filters deleted measurements and includes refreshed body-measurement weeks in incremental materialization.
Serving schema migration
src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.ts, src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.test.ts, src/db/clickhouse-migrations/registry.ts, src/db/clickhouse-migrations/registry.test.ts
Migration 0055 adds is_deleted and source_synced_at, skips absent serving tables, and is registered with coverage for its execution behavior.
End-to-end lifecycle validation
src/db/daily-body-measurement-read-model.integration.test.ts, docs/production-incident-baseline.md
Integration setup validates historical updates, tombstones, and dependent weekly results; incident records document validation outcomes and migration-table handling.
Serving query and test schema updates
packages/server/src/repositories/*, packages/server/src/routers/clickhouse-integration-test-*
Body-weight and cycling queries exclude deleted rows, while test schemas and read-model projections include lifecycle columns.

Estimated code review effort: 4 (Complex) | ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Propagate historical edits using source change/version timestamps rather than event-date windows [#1769] ✅
Emit tombstones for previously materialized measurements that are no longer live [#1769] ✅
Validate updates, deletions, and dependent weekly results with a real ClickHouse integration test [#1769] ✅

Possibly related PRs

  • Asherlc/dofek#1527: Related daily body measurement and weekly healthspan read-model behavior.
  • Asherlc/dofek#1579: Related ClickHouse view refresh metadata used by incremental materialization.

Suggested labels: area/server, area/db, type/bug

🚥 Pre-merge checks | ✅ 1 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title matches the change, but it misses the required area prefix format (e.g. [server]) and is not in the requested style. Rename it in imperative form with an area prefix, e.g. "[server] propagate historical body measurement changes".
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Propagate historical body measurement corrections via CDC watermarks

🐞 Bug fix 🧪 Tests ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Drive daily body refreshes from per-user CDC watermarks after view refresh succeeds.
• Emit tombstones for corrected/deleted historical measurement IDs and exclude deleted rows.
• Rematerialize affected healthspan weeks and migrate ClickHouse serving table schema.
Diagram

graph TD
  SRC{{"body_measurement_sample (CDC)"}} --> VIEW["v_body_measurement (refreshable view)"] --> DAILY[("daily_body_measurement") ] --> WEEK["weekly_healthspan"] --> SRV["Server repositories"]
  MIG["CH migration 0055"] --> DAILY
  subgraph Legend
    direction LR
    _ext{{"CDC source"}} ~~~ _tbl[("Serving table")] ~~~ _mod["Model/Service"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. ClickHouse DELETE mutations instead of tombstones
  • ➕ Keeps serving tables physically free of deleted rows
  • ➕ Simplifies downstream queries (no is_deleted predicate)
  • ➖ Mutations are operationally heavier and can lag under load
  • ➖ Harder to reason about incremental correctness and backfills
2. Switch to Collapsing/VersionedCollapsingMergeTree
  • ➕ Native collapse semantics for insert/delete pairs
  • ➕ Potentially clearer lifecycle modeling than manual tombstone column
  • ➖ Bigger table/engine change with higher migration risk
  • ➖ More complex query patterns and correctness footguns
3. Periodic full rebuild of daily_body_measurement
  • ➕ Simplest correctness model; avoids incremental watermark logic
  • ➕ Naturally handles historical corrections and deletions
  • ➖ Higher compute and longer refresh windows
  • ➖ Does not scale well as table grows

Recommendation: Keep the PR’s approach: per-user CDC watermarks + tombstones is a low-ops, mutation-free way to correctly propagate historical corrections/deletes while preserving incremental builds. The added source_synced_at and is_deleted columns make downstream exclusion explicit, and the weekly rematerialization hook closes the loop for dependent aggregates.

Files changed (16) +523 / -35

Bug fix (4) +147 / -28
daily_body_measurement.sqlRebuild incremental logic using CDC watermarks and tombstones +120/-28

Rebuild incremental logic using CDC watermarks and tombstones

• Replaces the prior rolling overlap strategy with per-user CDC sync watermarks gated by view refresh success time. Writes tombstones (is_deleted=1) when a previously served measurement disappears from the live view, and persists source_synced_at for incremental change detection.

analytics/models/read_models/daily_body_measurement.sql

weekly_healthspan.sqlExclude deleted body rows and rematerialize weeks impacted by refreshes +25/-0

Exclude deleted body rows and rematerialize weeks impacted by refreshes

• Filters daily_body_measurement by is_deleted=0 in weekly aggregation. Adds target_state/changed_body_weeks to force re-materialization for weeks touched by newly refreshed body rows during incremental runs.

analytics/models/read_models/weekly_healthspan.sql

body-clickhouse.tsFilter deleted rows in bodyWeightDedupClickHouseQuery +1/-0

Filter deleted rows in bodyWeightDedupClickHouseQuery

• Adds is_deleted = 0 to the serving query against analytics.daily_body_measurement FINAL so deleted/corrected historical IDs do not leak into results.

packages/server/src/repositories/body-clickhouse.ts

cycling-analytics-repository.tsExclude deleted measurements from baseline weight lookup +1/-0

Exclude deleted measurements from baseline weight lookup

• Adds is_deleted = 0 to the ClickHouse baseline weight CTE so trends are computed from live rows only.

packages/server/src/repositories/cycling-analytics-repository.ts

Tests (8) +325 / -7
read_model_microbatch.sql.test.tsUpdate microbatch SQL tests for watermark and deletion semantics +18/-2

Update microbatch SQL tests for watermark and deletion semantics

• Adjusts assertions to validate the new system.view_refreshes gating, changed_user_watermarks CTEs, and is_deleted filtering. Removes expectations around the old 7-day overlap window.

analytics/models/read_models/read_model_microbatch.sql.test.ts

body-clickhouse.test.tsAssert body weight queries exclude tombstoned measurements +1/-0

Assert body weight queries exclude tombstoned measurements

• Updates the repository query test to require an is_deleted = 0 predicate when reading from analytics.daily_body_measurement FINAL.

packages/server/src/repositories/body-clickhouse.test.ts

cycling-analytics-repository.integration.test.tsAdapt ClickHouse fixture inserts to new daily_body_measurement schema +2/-2

Adapt ClickHouse fixture inserts to new daily_body_measurement schema

• Updates test inserts to include is_deleted and source_synced_at columns in analytics.daily_body_measurement fixture rows.

packages/server/src/repositories/cycling-analytics-repository.integration.test.ts

cycling-analytics-repository.test.tsVerify cycling analytics query includes is_deleted filter +3/-0

Verify cycling analytics query includes is_deleted filter

• Adds an assertion that the mocked query reading daily_body_measurement FINAL includes AND is_deleted = 0.

packages/server/src/repositories/cycling-analytics-repository.test.ts

clickhouse-integration-test-read-models-b.tsAlign test read-model SQL with is_deleted/source watermark behavior +4/-0

Align test read-model SQL with is_deleted/source watermark behavior

• Adds explicit is_deleted/source_synced_at projections for daily_body_measurement and filters is_deleted = 0 in healthspan aggregation paths used by integration tests.

packages/server/src/routers/clickhouse-integration-test-read-models-b.ts

0055_daily_body_measurement_lifecycle.test.tsAdd unit test for daily_body_measurement lifecycle migration +17/-0

Add unit test for daily_body_measurement lifecycle migration

• Introduces a vitest asserting the migration adds is_deleted and source_synced_at columns with expected defaults and ordering.

src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.test.ts

registry.test.tsUpdate migration registry test to expect 0055 as latest +3/-3

Update migration registry test to expect 0055 as latest

• Adjusts assertions so the newest migration is 0055_daily_body_measurement_lifecycle and checks for the new column additions.

src/db/clickhouse-migrations/registry.test.ts

daily-body-measurement-read-model.integration.test.tsAdd end-to-end ClickHouse lifecycle integration test for historical fixes +277/-0

Add end-to-end ClickHouse lifecycle integration test for historical fixes

• Adds a real ClickHouse integration test that seeds CDC rows, refreshes the view, and materializes the read model both initial and incremental. Verifies historical updates overwrite prior values, deletes emit tombstones, and downstream weekly queries exclude deleted rows.

src/db/daily-body-measurement-read-model.integration.test.ts

Documentation (1) +33 / -0
production-incident-baseline.mdRecord local ClickHouse validation interruption details +33/-0

Record local ClickHouse validation interruption details

• Adds an incident baseline entry documenting a local Docker ClickHouse restart that interrupted a broad integration fixture. Captures evidence, suspected root cause, and validation status without changing product behavior.

docs/production-incident-baseline.md

Other (3) +18 / -0
clickhouse-integration-test-models.tsExtend integration-test DDL for daily_body_measurement lifecycle columns +2/-0

Extend integration-test DDL for daily_body_measurement lifecycle columns

• Adds is_deleted and source_synced_at to the test schema definition for analytics.daily_body_measurement to match production serving columns.

packages/server/src/routers/clickhouse-integration-test-models.ts

0055_daily_body_measurement_lifecycle.tsCreate ClickHouse migration adding is_deleted and source_synced_at +14/-0

Create ClickHouse migration adding is_deleted and source_synced_at

• Adds a new migration to alter analytics.daily_body_measurement with lifecycle (is_deleted) and CDC watermark (source_synced_at) columns.

src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.ts

registry.tsRegister 0055_daily_body_measurement_lifecycle migration +2/-0

Register 0055_daily_body_measurement_lifecycle migration

• Adds the 0055 factory import and appends it to the migrationFactories list so it runs after prior migrations.

src/db/clickhouse-migrations/registry.ts

@github-actions

github-actions Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Storybook previews for 3ff30474 are ready:

This comment updates automatically on each PR push.

@qodo-code-review

qodo-code-review Bot commented Jul 25, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 171 rules

Grey Divider


Action required

1. View watermark mismatch ✗ Dismissed 🐞 Bug ≡ Correctness
Description
daily_body_measurement advances per-user source_synced_at using
system.view_refreshes.last_success_time, but the rows it writes come from
analytics.v_body_measurement; if a source change is counted in the watermark before it’s actually
reflected in the refreshed view, the user watermark can move forward without materializing the row
and the incremental filter can skip it later.
Code

analytics/models/read_models/daily_body_measurement.sql[R13-45]

+body_view_state AS (
+    SELECT
+        toDateTime64(
+            coalesce(
+                max(last_success_time),
+                toDateTime('1970-01-01 00:00:00', 'UTC')
+            ),
+            9,
+            'UTC'
+        ) AS last_success_time
+    FROM system.view_refreshes
+    WHERE database = 'analytics'
+        AND view = 'v_body_measurement'
+),
+
+{% if is_incremental() %}
+target_user_state AS (
    SELECT
        user_id,
-        max(recorded_at) AS latest_recorded_at
-    FROM {{ this }}
+        max(source_synced_at) AS last_source_synced_at
+    FROM {{ this }} FINAL
    GROUP BY user_id
),
{% endif %}

-body_source AS (
+source_changes AS (
    SELECT
-        body.id AS measurement_id,
-        body.user_id AS user_id,
+        source.user_id AS user_id,
+        source._peerdb_synced_at AS source_synced_at
+    FROM analytics.body_measurement_sample AS source FINAL
+    CROSS JOIN body_view_state
+    WHERE source._peerdb_synced_at <= body_view_state.last_success_time
+),
Relevance

⭐⭐ Medium

Subtle view-refresh vs CDC watermark race; no close precedent, though team accepts incremental
watermark correctness fixes.

PR-#1527
PR-#1205

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The model’s watermark cutoff is derived from system.view_refreshes and applied to the source
table, while the actual rows written are pulled from analytics.v_body_measurement. The runbook
demonstrates system.view_refreshes provides both last_refresh_time and last_success_time, so
using last_success_time as a data-availability boundary is not inherently equivalent to “data
included in the view refresh.”

analytics/models/read_models/daily_body_measurement.sql[13-74]
docs/clickhouse-body-measurement-staleness-runbook.md[45-49]
PR-#1531

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`daily_body_measurement.sql` uses `system.view_refreshes.last_success_time` to bound `source_changes`, but `live_body` is sourced from `analytics.v_body_measurement`. This can let `changed_user_watermarks.source_synced_at` advance based on source rows that are not yet reflected in the view output used for materialization, which can prevent those rows from being picked up in later incremental runs.

### Issue Context
The staleness runbook shows `system.view_refreshes` exposes both `last_refresh_time` and `last_success_time`, indicating there are distinct timestamps available for the refresh window.

### Fix Focus Areas
- analytics/models/read_models/daily_body_measurement.sql[13-59]

### How to fix
- Prefer bounding `source_changes` by a timestamp that matches the view’s refresh snapshot boundary (e.g., `last_refresh_time` rather than `last_success_time`), or
- Add a small “safety overlap” so incremental runs re-check a recent synced-at window (so a change counted during refresh can still be materialized once it appears in the view), and/or
- Ensure the per-user watermark only advances to the max `_peerdb_synced_at` that is guaranteed to be represented in `analytics.v_body_measurement` for that refresh.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Inline ClickHouse SQL in router ✗ Dismissed 📘 Rule violation ⚙ Maintainability
Description
Non-trivial ClickHouse transformation SQL is embedded as multiline template literals in
packages/server/src/routers/clickhouse-integration-test-read-models-b.ts (and was modified in this
PR). This violates the requirement to keep analytics transformation SQL in dedicated .sql model
files, increasing drift/duplication risk versus the canonical dbt models.
Code

packages/server/src/routers/clickhouse-integration-test-read-models-b.ts[R367-368]

+  0 AS is_deleted,
+  refresh_clock.refreshed_at AS source_synced_at,
Relevance

⭐⭐⭐ High

Team has accepted router-layer SQL compliance/architecture refactors to reduce drift/duplication.

PR-#1158

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 784482 requires ClickHouse analytics transformation SQL to be stored in dedicated
.sql model files under analytics/models/ rather than embedded in TypeScript. The cited router
file contains (and this PR updates) multiline ClickHouse transformation SQL strings (e.g., `WITH ...
SELECT ...`) for body measurement and healthspan read models.

Rule 784482: Store ClickHouse analytics transformation SQL in dedicated .sql model files
packages/server/src/routers/clickhouse-integration-test-read-models-b.ts[341-372]
packages/server/src/routers/clickhouse-integration-test-read-models-b.ts[469-523]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR modifies ClickHouse transformation queries that are still defined inline as multiline template strings in TypeScript, instead of being sourced from dedicated `.sql` model files under `analytics/models/`.

## Issue Context
Compliance requires ClickHouse analytics transformation SQL (non-trivial SELECT/CTE queries) to live in `.sql` files, with TypeScript only loading/referencing them, to prevent query drift and duplication.

## Fix Focus Areas
- packages/server/src/routers/clickhouse-integration-test-read-models-b.ts[341-372]
- packages/server/src/routers/clickhouse-integration-test-read-models-b.ts[469-523]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Changed weeks broad scan ✓ Resolved 🐞 Bug ➹ Performance
Description
weekly_healthspan detects changed_body_weeks by filtering daily_body_measurement on
refreshed_at, but daily_body_measurement is ordered by (user_id, measurement_id), so this
predicate may require scanning a large portion of the table (and FINAL adds extra cost).
Code

analytics/models/read_models/weekly_healthspan.sql[R113-120]

+{% if is_incremental() %}
+changed_body_weeks AS (
+    SELECT DISTINCT
+        user_id,
+        toMonday(date) AS week_start
+    FROM {{ ref('daily_body_measurement') }} FINAL
+    WHERE refreshed_at > (SELECT last_refreshed_at FROM target_state)
+),
Relevance

⭐⭐ Medium

Perf concern plausible, but mixed history: they sometimes accept scan-reduction, yet rejected
similar incremental FINAL-scan optimization.

PR-#1610
PR-#1584

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
weekly_healthspan filters daily_body_measurement by refreshed_at to compute changed weeks,
while daily_body_measurement’s ORDER BY does not include refreshed_at, making it plausible this
filter cannot use the primary key ordering to prune efficiently.

analytics/models/read_models/weekly_healthspan.sql[102-175]
analytics/models/read_models/daily_body_measurement.sql[1-6]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`changed_body_weeks` relies on `WHERE refreshed_at > last_refreshed_at` over `daily_body_measurement FINAL`. Since the serving table’s primary sort key is `(user_id, measurement_id)`, filtering by `refreshed_at` may not prune effectively and can lead to broad scans.

### Issue Context
This query runs on incremental weekly healthspan builds, so its cost can repeat frequently as history grows.

### Fix Focus Areas
- analytics/models/read_models/weekly_healthspan.sql[113-175]
- analytics/models/read_models/daily_body_measurement.sql[1-6]

### How to fix
- Add a ClickHouse data-skipping index / projection that supports `refreshed_at` filtering for `daily_body_measurement`, or
- Materialize a small “dirty weeks” helper table (user_id, week_start, refreshed_at/source_synced_at) during daily-body refresh and have `weekly_healthspan` read from that instead of scanning the full serving table.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

4. Full-history source scan 🐞 Bug ➹ Performance
Description
On incremental runs, daily_body_measurement computes changed_user_watermarks by scanning
analytics.body_measurement_sample FINAL up to the view cutoff without any lower bound, so every
run can read/deduplicate the full source history before it can decide which users changed.
Code

analytics/models/read_models/daily_body_measurement.sql[R38-59]

+source_changes AS (
    SELECT
-        body.id AS measurement_id,
-        body.user_id AS user_id,
+        source.user_id AS user_id,
+        source._peerdb_synced_at AS source_synced_at
+    FROM analytics.body_measurement_sample AS source FINAL
+    CROSS JOIN body_view_state
+    WHERE source._peerdb_synced_at <= body_view_state.last_success_time
+),
+
+changed_user_watermarks AS (
+    SELECT
+        source_changes.user_id AS user_id,
+        max(source_changes.source_synced_at) AS source_synced_at
+    FROM source_changes
+    {% if is_incremental() %}
+    LEFT JOIN target_user_state
+        ON target_user_state.user_id = source_changes.user_id
+    WHERE target_user_state.user_id IS NULL
+        OR source_changes.source_synced_at > target_user_state.last_source_synced_at
+    {% endif %}
+    GROUP BY source_changes.user_id
+),
Relevance

⭐ Low

Similar “avoid full-table FINAL scans on incremental” perf suggestion was rejected previously.

PR-#1610

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
source_changes reads from analytics.body_measurement_sample FINAL and is only upper-bounded by
the view time; the incremental comparison to target_user_state happens later, meaning the
scan/aggregation happens regardless. The runbook notes large FINAL scans can saturate ClickHouse.

analytics/models/read_models/daily_body_measurement.sql[38-59]
docs/clickhouse-body-measurement-staleness-runbook.md[87-90]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Incremental builds still scan `analytics.body_measurement_sample FINAL` broadly because the only incremental filter is applied after aggregation (in `changed_user_watermarks` via a join to `target_user_state`). This can make incremental runs scale with total history.

### Issue Context
The repo runbook explicitly warns about host-saturating queries that scan entire `FINAL` tables.

### Fix Focus Areas
- analytics/models/read_models/daily_body_measurement.sql[28-59]
- docs/clickhouse-body-measurement-staleness-runbook.md[87-90]

### How to fix
- Add a lower-bound predicate on `source._peerdb_synced_at` for incremental runs (e.g., `> (SELECT min(last_source_synced_at) FROM target_user_state)` or another safe global bound) so ClickHouse can skip older parts.
- Alternatively maintain a small per-user change log / watermark table to avoid scanning the full `body_measurement_sample` history every run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread analytics/models/read_models/daily_body_measurement.sql
Comment thread analytics/models/read_models/weekly_healthspan.sql
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/server/src/routers/clickhouse-integration-test-read-models-b.ts (1)

469-523: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Missing FINAL on daily_body_measurement reads in the same function that correctly uses it elsewhere.

Lines 482 (healthspan_activity_zone_minutes FINAL) and 509 correctly append FINAL for a ReplacingMergeTree table, but the two daily_body_measurement reads at lines 490 and 520 omit it, despite daily_body_measurement using the same ReplacingMergeTree(refresh_version) engine (per the dbt model config). Without FINAL, a stale un-merged pre-tombstone version (is_deleted = 0) of a row can coexist with its newer tombstoned version and still pass the WHERE is_deleted = 0 filter added here — exactly the bug this PR is meant to prevent, just relocated into the test fixture.

🐛 Proposed fix
   SELECT
     user_id,
     toMonday(date) AS week_start
-  FROM ${databases.analytics}.daily_body_measurement
+  FROM ${databases.analytics}.daily_body_measurement FINAL
   WHERE is_deleted = 0
   GROUP BY user_id, toMonday(date)
 ),
@@
   argMax(weight_kg, (recorded_at, refresh_version, measurement_id)) AS weight_kg,
   argMax(body_fat_pct, (recorded_at, refresh_version, measurement_id)) AS body_fat_pct
-  FROM ${databases.analytics}.daily_body_measurement
+  FROM ${databases.analytics}.daily_body_measurement FINAL
   WHERE is_deleted = 0
   GROUP BY user_id, toMonday(date)
 ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routers/clickhouse-integration-test-read-models-b.ts`
around lines 469 - 523, Update both daily_body_measurement reads in
buildTestHealthspanReadModelSelectSql—within week_keys and body_by_week—to
append FINAL, matching the existing healthspan_activity_zone_minutes read while
preserving the current filters and aggregations.
packages/server/src/routers/clickhouse-integration-test-models.ts (1)

177-186: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Test table's ORDER BY for daily_body_measurement diverges from production and undermines dedup-parity for exactly the scenario this PR fixes.

Line 558 defines order_by as (user_id, recorded_at, measurement_id), but the actual dbt model (analytics/models/read_models/daily_body_measurement.sql) uses order_by='(user_id, measurement_id)'. Since measurement_id is already a globally-unique UUID, including recorded_at in the test table's key serves no purpose, but it does change ReplacingMergeTree dedup semantics: if a test scenario corrects a recorded_at timestamp for an existing measurement_id (part of the "historical edits" behavior this PR targets), production would replace the row via FINAL, while this test table would treat the two versions as distinct rows and never deduplicate them, even with FINAL. That's a material behavior gap for a table whose ordering directly affects test-vs-prod parity for corrections.

🐛 Proposed fix
                       : shortViewName === "daily_body_measurement"
-                        ? "(user_id, recorded_at, measurement_id)"
+                        ? "(user_id, measurement_id)"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/server/src/routers/clickhouse-integration-test-models.ts` around
lines 177 - 186, The test table definition for daily_body_measurement must match
production’s ReplacingMergeTree key: update its order_by configuration from
(user_id, recorded_at, measurement_id) to (user_id, measurement_id), preserving
deduplication when recorded_at changes for an existing measurement_id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@analytics/models/read_models/daily_body_measurement.sql`:
- Around line 12-153: Review the incremental flow around target_user_state,
existing_rows, and live_body to avoid repeatedly scanning full user history with
FINAL as volume grows. Add materialization-duration monitoring and reassess the
max_threads: 1 setting so daily_body_measurement can use increased parallelism
when performance requires it.
- Around line 13-26: The body_view_state CTE currently reads last_success_time
from system.view_refreshes for v_body_measurement, but that object is a plain
view and has no refresh watermark. Replace this dependency with a real upstream
watermark that advances for body measurements, and update the source_changes
filtering flow to use it; alternatively, convert v_body_measurement back to a
refreshable materialized view so its refresh timestamp is valid.

In `@src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.ts`:
- Line 5: Update tableCountRowsSchema to validate string and numeric counts as
finite, nonnegative numbers rather than accepting arbitrary strings, ensuring
malformed values are rejected before the zero comparison. In the migration’s
table-count query flow, replace raw database result handling with
executeWithSchema() using tableCountRowsSchema so the validated result is used
and untyped SQL output is not consumed directly.

In `@src/db/daily-body-measurement-read-model.integration.test.ts`:
- Around line 164-179: Replace readDependentWeeklyWeight with a helper that
materializes and queries the production weekly_healthspan model using the
existing model/materialization utilities. Have the integration test assert the
model’s weekly output after both update and deletion lifecycle changes, rather
than reimplementing the daily_body_measurement aggregation.

---

Outside diff comments:
In `@packages/server/src/routers/clickhouse-integration-test-models.ts`:
- Around line 177-186: The test table definition for daily_body_measurement must
match production’s ReplacingMergeTree key: update its order_by configuration
from (user_id, recorded_at, measurement_id) to (user_id, measurement_id),
preserving deduplication when recorded_at changes for an existing
measurement_id.

In `@packages/server/src/routers/clickhouse-integration-test-read-models-b.ts`:
- Around line 469-523: Update both daily_body_measurement reads in
buildTestHealthspanReadModelSelectSql—within week_keys and body_by_week—to
append FINAL, matching the existing healthspan_activity_zone_minutes read while
preserving the current filters and aggregations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d5e76b6a-bce9-4311-b7df-00ac1790f4e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6f1c66c and 879d558.

📒 Files selected for processing (16)
  • analytics/models/read_models/daily_body_measurement.sql
  • analytics/models/read_models/read_model_microbatch.sql.test.ts
  • analytics/models/read_models/weekly_healthspan.sql
  • docs/production-incident-baseline.md
  • packages/server/src/repositories/body-clickhouse.test.ts
  • packages/server/src/repositories/body-clickhouse.ts
  • packages/server/src/repositories/cycling-analytics-repository.integration.test.ts
  • packages/server/src/repositories/cycling-analytics-repository.test.ts
  • packages/server/src/repositories/cycling-analytics-repository.ts
  • packages/server/src/routers/clickhouse-integration-test-models.ts
  • packages/server/src/routers/clickhouse-integration-test-read-models-b.ts
  • src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.test.ts
  • src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.ts
  • src/db/clickhouse-migrations/registry.test.ts
  • src/db/clickhouse-migrations/registry.ts
  • src/db/daily-body-measurement-read-model.integration.test.ts

Comment thread analytics/models/read_models/daily_body_measurement.sql
Comment thread analytics/models/read_models/daily_body_measurement.sql
Comment thread src/db/clickhouse-migrations/0055_daily_body_measurement_lifecycle.ts Outdated
Comment thread src/db/daily-body-measurement-read-model.integration.test.ts Outdated
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc

Asherlc commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Addressed the two actionable outside-diff findings in 951fdbc: the isolated weekly-healthspan fixture now reads both daily_body_measurement inputs with FINAL, and its daily_body_measurement table uses the production (user_id, measurement_id) sort key. The helper regression test asserts both invariants. Validation: 10 focused unit tests, root typecheck, analytics policy, Biome, and the 2-test real ClickHouse lifecycle suite all pass.

@Asherlc
Asherlc enabled auto-merge (squash) July 25, 2026 23:57
@codereviewbot-ai

Copy link
Copy Markdown

🤖 Review skipped: Repository rate limit exceeded. Free accounts are limited to 2 reviews per 4 hours per repository. Upgrade to a paid plan for unlimited reviews.

@Asherlc
Asherlc merged commit a5c6b48 into main Jul 26, 2026
101 checks passed
@Asherlc
Asherlc deleted the codex/issue-1769 branch July 26, 2026 00:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Propagate historical body-measurement edits and deletions into the daily serving model

2 participants