Skip to content

[codex] Move provider stats fully to ClickHouse - #1098

Merged
Asherlc merged 2 commits into
mainfrom
foam-pluto
May 7, 2026
Merged

Asherlc merged 2 commits into
mainfrom
foam-pluto

Conversation

@Asherlc

@Asherlc Asherlc commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Move sync.providerStats entirely onto ClickHouse analytics.provider_stats
  • Add raw ClickHouse mirrors and PeerDB CDC coverage for provider inventory tables
  • Add ClickHouse migration 0008_complete_provider_stats_raw_mirrors and update docs/incident notes

Root Cause

Production Postgres pool starvation made cheap auth/session queries fail while expensive provider-stat and dashboard reads held pool clients. The provider stats endpoint was still doing per-provider Postgres count subqueries over large raw tables.

Validation

  • pnpm lint
  • pnpm tsc --noEmit
  • cd packages/server && pnpm tsc --noEmit
  • cd packages/web && pnpm tsc --noEmit
  • focused unit tests for sync/ClickHouse/CDC
  • pnpm test run packages/server/src/routers/router-sql.integration.test.ts --project integration
  • providerStats integration slice
  • pnpm test:changed

Summary by CodeRabbit

  • Documentation

    • Updated deployment runbook to include ClickHouse/PeerDB bootstrap step
    • Expanded provider inventory docs to cover additional record types (food entries, health events, lab panels/results, journal entries)
    • Added production incident baseline (2026-05-07) describing auth/tRPC failures and mitigations
  • Performance Improvements

    • Provider statistics now read from the analytics (ClickHouse) layer with broader coverage
  • Tests

    • Expanded test coverage to validate the new analytics/mirroring behavior and sync targets

@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request routes provider statistics reads from Postgres to a ClickHouse read model by adding five new raw ClickHouse tables and a PeerDB mirror, expanding analytics.provider_stats, splitting/executing PeerDB SQL statements, adding a migration, updating SyncRepository to use a ClickHouse store, wiring the tRPC endpoint, updating tests, and documenting the changes.

Changes

Provider Stats ClickHouse Migration

Layer / File(s) Summary
Data Contracts
packages/server/src/repositories/sync-repository.ts
New ClickHouse provider stats row schema and ProviderStatsClickHouseStore interface define the data contract for external ClickHouse queries.
Raw Table Definitions
src/db/clickhouse-raw-tables.ts
Five new CREATE TABLE statements for food_entry, health_event, lab_panel, lab_result, and journal_entry follow existing raw table patterns with ReplacingMergeTree keys.
Read Model & Aggregations
src/db/clickhouse-read-models.ts
The analytics.provider_stats read model expands provider discovery to include new table sources and adds aggregation CTEs for the additional entry types, with real counts instead of placeholder casts.
CDC Mirror Definition
src/db/peerdb/metric-stream-cdc.sql
New dofek_provider_inventory_raw_analytics mirror replicates the five new table types from Postgres to ClickHouse with initial copy and soft-delete enabled.
CDC Setup & Execution
src/db/clickhouse-cdc.ts
Extend analyticsSourceTables list and add quote-aware SQL statement splitting for sequential PeerDB execution instead of executing the rendered script as a whole.
Migration Infrastructure
src/db/clickhouse-migrations.ts
New migration entry 0008_complete_provider_stats_raw_mirrors orchestrates statement generation from raw table and read-model builders.
Repository Implementation
packages/server/src/repositories/sync-repository.ts
Constructor accepts optional ClickHouse providerStatsStore; getProviderStats() delegates to a new private #getClickHouseProviderStats() method that queries the ClickHouse read model instead of Postgres.
Router Wiring
packages/server/src/routers/sync.ts
The providerStats tRPC endpoint requires ctx.sensorStore, adds an output schema, throws PRECONDITION_FAILED when missing, and constructs SyncRepository with the sensorStore.
Tests
packages/server/src/repositories/sync-repository.test.ts, packages/server/src/routers/sync.test.ts, packages/server/src/routers/clickhouse-integration-test-helpers.ts, src/db/clickhouse-cdc.test.ts, src/db/clickhouse-migrations.test.ts, src/db/clickhouse.test.ts
Refactor test fixtures to supply ClickHouse rows via sensorStore.query; update assertions for new raw tables, mirrors, and migration counts; verify Postgres execute is not called where applicable.
Documentation
deploy/README.md, docs/clickhouse-metric-stream.md, docs/production-incident-baseline.md
Document PeerDB/ClickHouse bootstrap step, clarify provider inventory sourcing from ClickHouse read model, and add incident baseline entry for 2026-05-07 Postgres pool starvation with mitigation details.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Asherlc/dofek#1087: Modifies ClickHouse CDC bootstrap and SQL execution splitting; related to setupClickHouseCdc changes.
  • Asherlc/dofek#1097: Parallel ClickHouse migration and SyncRepository wiring changes affecting provider-stats and read-models.
  • Asherlc/dofek#1089: Changes PeerDB mirror definitions and tests related to metric-stream/analytics mirroring.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Move provider stats fully to ClickHouse' accurately and concisely describes the main change: migrating the provider statistics feature from Postgres to ClickHouse, which is the primary objective across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch foam-pluto

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

@Asherlc
Asherlc marked this pull request as ready for review May 7, 2026 17:35
Copilot AI review requested due to automatic review settings May 7, 2026 17:35
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Storybook previews for 3fb267c7 are ready:

This comment updates automatically on each PR push.

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.

Pull request overview

This PR completes the migration of sync.providerStats from Postgres to the ClickHouse analytics.provider_stats read model, and expands PeerDB CDC + raw ClickHouse mirror coverage so provider inventory tables are available in ClickHouse.

Changes:

  • Route sync.providerStats to ClickHouse-backed counts instead of per-provider Postgres subqueries.
  • Add new PeerDB mirror + ClickHouse raw tables for provider inventory (food_entry, health_event, lab_panel, lab_result, journal_entry).
  • Add ClickHouse migration 0008_complete_provider_stats_raw_mirrors and update operational/docs notes.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/db/peerdb/metric-stream-cdc.sql Adds a dedicated PeerDB mirror for provider-inventory tables into postgres_fitness in ClickHouse.
src/db/clickhouse.test.ts Extends bootstrap SQL assertions to include new raw tables and provider_stats logic.
src/db/clickhouse-read-models.ts Updates analytics.provider_stats to count inventory tables; introduces provider_stats statement builder used by migrations/bootstrap.
src/db/clickhouse-raw-tables.ts Adds ClickHouse raw table DDL for provider inventory tables under postgres_fitness.*.
src/db/clickhouse-migrations.ts Adds migration 0008_complete_provider_stats_raw_mirrors to create raw tables and rebuild provider_stats.
src/db/clickhouse-migrations.test.ts Updates expected migration output and migration count.
src/db/clickhouse-cdc.ts Adds provider inventory tables to CDC publication/table lists.
src/db/clickhouse-cdc.test.ts Updates CDC setup expectations to include the new mirror and updated query counts.
packages/server/src/routers/sync.ts Passes ctx.sensorStore into SyncRepository for providerStats.
packages/server/src/routers/sync.test.ts Updates providerStats tests to validate ClickHouse path and that Postgres isn’t queried.
packages/server/src/routers/clickhouse-integration-test-helpers.ts Adds raw-table sync support for new provider inventory tables in ClickHouse integration tests.
packages/server/src/routers/clickhouse-integration-test-helpers.test.ts Extends assertions for new truncate/insert commands.
packages/server/src/repositories/sync-repository.ts Replaces Postgres provider stats query with ClickHouse analytics.provider_stats query via injected store.
packages/server/src/repositories/sync-repository.test.ts Updates unit tests to validate ClickHouse stats path and mapping behavior.
docs/production-incident-baseline.md Adds incident write-up documenting Postgres pool starvation and the mitigation.
docs/clickhouse-metric-stream.md Documents provider inventory counts being sourced from analytics.provider_stats and new mirrors.
deploy/README.md Updates deploy step docs to reflect multiple mirrors applied during CDC setup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/server/src/routers/sync.ts Outdated
Comment thread packages/server/src/repositories/sync-repository.ts Outdated
Comment thread src/db/clickhouse-read-models.ts

@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: 1

🧹 Nitpick comments (2)
src/db/clickhouse-read-models.ts (1)

771-788: ⚡ Quick win

Collapse the two food_entry aggregations into one scan.

food_entry_counts and nutrition_daily_counts both read postgres_fitness.food_entry FINAL, so every analytics.provider_stats refresh scans that table twice. Since this view is refreshed continuously, it’s cheaper to compute count() and uniqExact(date) in the same CTE and join once.

♻️ Suggested shape
-food_entry_counts AS (
-  SELECT user_id, provider_id, count() AS count
+food_entry_counts AS (
+  SELECT
+    user_id,
+    provider_id,
+    count() AS food_entries,
+    uniqExact(date) AS nutrition_daily
   FROM postgres_fitness.food_entry FINAL
   WHERE _peerdb_is_deleted = 0
   GROUP BY user_id, provider_id
 ),
 health_event_counts AS (
   SELECT user_id, provider_id, count() AS count
   FROM postgres_fitness.health_event FINAL
   WHERE _peerdb_is_deleted = 0
   GROUP BY user_id, provider_id
 ),
-nutrition_daily_counts AS (
-  SELECT user_id, provider_id, uniqExact(date) AS count
-  FROM postgres_fitness.food_entry FINAL
-  WHERE _peerdb_is_deleted = 0
-  GROUP BY user_id, provider_id
-),
 lab_panel_counts AS (
   SELECT user_id, provider_id, count() AS count
   FROM postgres_fitness.lab_panel FINAL
   WHERE _peerdb_is_deleted = 0
   GROUP BY user_id, provider_id
 )
 ...
-  coalesce(food_entry_counts.count, 0) AS food_entries,
+  coalesce(food_entry_counts.food_entries, 0) AS food_entries,
   coalesce(health_event_counts.count, 0) AS health_events,
   coalesce(metric_stream_counts.count, 0) AS metric_stream,
-  coalesce(nutrition_daily_counts.count, 0) AS nutrition_daily,
+  coalesce(food_entry_counts.nutrition_daily, 0) AS nutrition_daily,
   coalesce(lab_panel_counts.count, 0) AS lab_panels,
   coalesce(lab_result_counts.count, 0) AS lab_results,
   coalesce(journal_entry_counts.count, 0) AS journal_entries
 ...
-LEFT JOIN nutrition_daily_counts
-  ON nutrition_daily_counts.user_id = providers.user_id
- AND nutrition_daily_counts.provider_id = providers.provider_id
 LEFT JOIN lab_panel_counts
   ON lab_panel_counts.user_id = providers.user_id
  AND lab_panel_counts.provider_id = providers.provider_id

Also applies to: 817-818, 843-845

packages/server/src/routers/clickhouse-integration-test-helpers.test.ts (1)

129-136: ⚡ Quick win

Expand provider-inventory INSERT coverage in this helper test.

Only .lab_result is asserted in the new insert-path checks; .health_event and .lab_panel regressions could slip through here.

Proposed test assertion additions
     expect(
       commands.some(
         (command) =>
           command.includes("INSERT INTO postgres_fitness_test_") &&
           command.includes(".lab_result") &&
           command.includes("FROM postgresql('db:5432', 'health', 'lab_result'"),
       ),
     ).toBe(true);
+    expect(
+      commands.some(
+        (command) =>
+          command.includes("INSERT INTO postgres_fitness_test_") &&
+          command.includes(".health_event") &&
+          command.includes("FROM postgresql('db:5432', 'health', 'health_event'"),
+      ),
+    ).toBe(true);
+    expect(
+      commands.some(
+        (command) =>
+          command.includes("INSERT INTO postgres_fitness_test_") &&
+          command.includes(".lab_panel") &&
+          command.includes("FROM postgresql('db:5432', 'health', 'lab_panel'"),
+      ),
+    ).toBe(true);
🤖 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-helpers.test.ts`
around lines 129 - 136, The test currently only asserts that commands.some(...)
includes an INSERT referencing ".lab_result"; update the assertion in
clickhouse-integration-test-helpers.test.ts (the commands.some check) to also
verify INSERTs for ".health_event" and ".lab_panel" by adding similar includes
checks for ".health_event" and ".lab_panel" (alongside the existing
".lab_result") and ensure the postgresql(...) source strings match the
corresponding table names (e.g., postgresql('db:5432', 'health', 'health_event')
and postgresql('db:5432', 'health', 'lab_panel')); keep the overall
commands.some(...) structure so the test fails if any of the three INSERT paths
are missing.
🤖 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 `@packages/server/src/routers/sync.ts`:
- Around line 315-317: The providerStats procedure (created via
cachedProtectedQuery with CacheTTL.SHORT) is missing a Zod output schema; update
the procedure chain for providerStats to call .output(...) with a Zod schema
that matches the shape returned by SyncRepository.getProviderStats() (e.g., an
array/object fields returned from getProviderStats) so the tRPC output is
validated; ensure the schema is imported from zod and attached to the same
providerStats definition where cachedProtectedQuery(...)...query(...) is
declared.

---

Nitpick comments:
In `@packages/server/src/routers/clickhouse-integration-test-helpers.test.ts`:
- Around line 129-136: The test currently only asserts that commands.some(...)
includes an INSERT referencing ".lab_result"; update the assertion in
clickhouse-integration-test-helpers.test.ts (the commands.some check) to also
verify INSERTs for ".health_event" and ".lab_panel" by adding similar includes
checks for ".health_event" and ".lab_panel" (alongside the existing
".lab_result") and ensure the postgresql(...) source strings match the
corresponding table names (e.g., postgresql('db:5432', 'health', 'health_event')
and postgresql('db:5432', 'health', 'lab_panel')); keep the overall
commands.some(...) structure so the test fails if any of the three INSERT paths
are missing.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a654b39-2c16-439a-b5a2-e9977bee22ed

📥 Commits

Reviewing files that changed from the base of the PR and between 2667575 and c645be1.

📒 Files selected for processing (17)
  • deploy/README.md
  • docs/clickhouse-metric-stream.md
  • docs/production-incident-baseline.md
  • packages/server/src/repositories/sync-repository.test.ts
  • packages/server/src/repositories/sync-repository.ts
  • packages/server/src/routers/clickhouse-integration-test-helpers.test.ts
  • packages/server/src/routers/clickhouse-integration-test-helpers.ts
  • packages/server/src/routers/sync.test.ts
  • packages/server/src/routers/sync.ts
  • src/db/clickhouse-cdc.test.ts
  • src/db/clickhouse-cdc.ts
  • src/db/clickhouse-migrations.test.ts
  • src/db/clickhouse-migrations.ts
  • src/db/clickhouse-raw-tables.ts
  • src/db/clickhouse-read-models.ts
  • src/db/clickhouse.test.ts
  • src/db/peerdb/metric-stream-cdc.sql

Comment thread packages/server/src/routers/sync.ts Outdated
@github-actions

github-actions Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review app is ready:

This environment runs on a dedicated Hetzner server for PR #1098 and updates on each push.

@Asherlc
Asherlc enabled auto-merge (squash) May 7, 2026 18:00

@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.

🧹 Nitpick comments (1)
packages/server/src/routers/sync.test.ts (1)

1163-1209: ⚡ Quick win

Assert the ClickHouse query stays user-scoped.

This test proves the mapping, but it doesn't protect the userId filter. Please also assert that sensorStore.query is called with the current user param so a future regression can't drop the per-user boundary and still pass.

Suggested test hardening
-    it("maps ClickHouse rows to provider stats", async () => {
-      const execute = vi.fn().mockResolvedValue([]);
+    it("maps ClickHouse rows to provider stats", async () => {
+      const execute = vi.fn().mockResolvedValue([]);
+      const query = vi.fn().mockResolvedValue([
+        {
+          provider_id: "wahoo",
+          activities: 10,
+          daily_metrics: 5,
+          sleep_sessions: 3,
+          body_measurements: 2,
+          food_entries: 8,
+          health_events: 1,
+          metric_stream: 100,
+          nutrition_daily: 7,
+          lab_panels: 2,
+          lab_results: 4,
+          journal_entries: 6,
+        },
+      ]);
       const caller = createCaller({
         db: {
           execute,
         },
         sensorStore: {
-          query: vi.fn().mockResolvedValue([
-            {
-              provider_id: "wahoo",
-              activities: 10,
-              daily_metrics: 5,
-              sleep_sessions: 3,
-              body_measurements: 2,
-              food_entries: 8,
-              health_events: 1,
-              metric_stream: 100,
-              nutrition_daily: 7,
-              lab_panels: 2,
-              lab_results: 4,
-              journal_entries: 6,
-            },
-          ]),
+          query,
         },
         userId: "user-1",
         timezone: "UTC",
       });
@@
       expect(result).toEqual([
         {
           providerId: "wahoo",
@@
         },
       ]);
       expect(execute).not.toHaveBeenCalled();
+      expect(query).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.stringContaining("FROM analytics.provider_stats"),
+        { userId: "user-1" },
+      );
     });
🤖 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/sync.test.ts` around lines 1163 - 1209, The test
currently verifies mapping but not that the ClickHouse query is scoped to the
current user; update the "maps ClickHouse rows to provider stats" test to assert
that sensorStore.query was invoked with the current user parameter (e.g. userId
"user-1") so the per-user filter is preserved — locate the caller constructed by
createCaller and add an assertion that sensorStore.query (the mock passed into
caller) was called with an argument object containing the userId (or equivalent
key used by providerStats) to ensure providerStats enforces user scoping.
🤖 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.

Nitpick comments:
In `@packages/server/src/routers/sync.test.ts`:
- Around line 1163-1209: The test currently verifies mapping but not that the
ClickHouse query is scoped to the current user; update the "maps ClickHouse rows
to provider stats" test to assert that sensorStore.query was invoked with the
current user parameter (e.g. userId "user-1") so the per-user filter is
preserved — locate the caller constructed by createCaller and add an assertion
that sensorStore.query (the mock passed into caller) was called with an argument
object containing the userId (or equivalent key used by providerStats) to ensure
providerStats enforces user scoping.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 81170ce1-7ef8-476c-b68b-eb64e5236208

📥 Commits

Reviewing files that changed from the base of the PR and between c645be1 and 3d75715.

📒 Files selected for processing (4)
  • packages/server/src/repositories/sync-repository.ts
  • packages/server/src/routers/sync.test.ts
  • packages/server/src/routers/sync.ts
  • src/db/clickhouse-read-models.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/server/src/routers/sync.ts

@Asherlc
Asherlc merged commit edcd2a2 into main May 7, 2026
67 checks passed
@Asherlc
Asherlc deleted the foam-pluto branch May 7, 2026 18:12
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.

2 participants