Skip to content

Add metric stream ID primary key migration - #1079

Merged
Asherlc merged 3 commits into
mainfrom
Asherlc/metric-stream-id-pr
May 1, 2026
Merged

Asherlc merged 3 commits into
mainfrom
Asherlc/metric-stream-id-pr

Conversation

@Asherlc

@Asherlc Asherlc commented May 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • defer the earlier metric_stream migration to add only id default plus REPLICA IDENTITY FULL
  • add a chunk-aware metric_stream.id backfill, enforce id NOT NULL, and create the hypertable-compatible primary key on (id, recorded_at)
  • add a primary key for fitness.oauth_token using the existing unique user/provider index
  • update migration tests for compressed Timescale chunks, replica identity, and missing primary keys

Validation

  • pnpm vitest run src/db/migrate.test.ts src/db/migrate.integration.test.ts src/db/metric-stream-replica-identity.integration.test.ts
  • pnpm lint
  • pnpm tsc --noEmit
  • cd packages/server && pnpm tsc --noEmit
  • cd packages/web && pnpm tsc --noEmit
  • pnpm test:changed
  • TEST_DATABASE_URL=postgres://health:health@127.0.0.1:5435/health pnpm test

Note: the first full pnpm test attempt hit a local Testcontainers startup failure (No host port found for host IP) in fatsecret-sync.integration.test.ts. That suite passed in isolation, then the full suite passed against the shared local Timescale URL above.

Summary by CodeRabbit

  • Database Schema Updates

    • Introduced UUID id with automatic generation for metric stream, ensured no-null ids and a composite primary key on (id, recorded_at); adjusted replica identity behavior.
    • Added a stable composite primary key for the OAuth token table.
  • Testing

    • Added and updated integration tests covering migrations, replica identity, nullability, compression behavior, backfill, and final primary-key constraints.

Copilot AI review requested due to automatic review settings May 1, 2026 21:15
@coderabbitai

coderabbitai Bot commented May 1, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2239000c-86c6-4ff4-8c70-35958388aee4

📥 Commits

Reviewing files that changed from the base of the PR and between 9327bb6 and 3d0ccf9.

📒 Files selected for processing (2)
  • drizzle/0010_oauth_token_primary_key.sql
  • src/db/metric-stream-replica-identity.integration.test.ts
✅ Files skipped from review due to trivial changes (1)
  • drizzle/0010_oauth_token_primary_key.sql
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/db/metric-stream-replica-identity.integration.test.ts

📝 Walkthrough

Walkthrough

Split the metric_stream primary-key work into: migration 0007 (add id column + default and set replica identity at schema level) and migration 0009 (chunked backfill of NULL ids, set NOT NULL, add composite PK). Add idempotent oauth_token PK migration and update integration/unit tests.

Changes

Cohort / File(s) Summary
Metric stream migrations
drizzle/0007_metric_stream_primary_key.sql, drizzle/0009_metric_stream_id_not_null_primary_key.sql
0007: add id uuid column with gen_random_uuid() default and set replica identity to FULL at schema level (no backfill). 0009: add fitness.backfill_metric_stream_ids(batch_size) procedure to batch-update NULL ids across TimescaleDB chunks (decompress/update/recompress as needed), verify no NULLs remain, set id NOT NULL, and create composite PK (id, recorded_at) if absent.
OAuth token migration
drizzle/0010_oauth_token_primary_key.sql
Idempotent DO block that checks for an existing primary key on fitness.oauth_token and, if missing, creates oauth_token_pkey bound to the existing oauth_token_user_provider_uidx index.
Integration tests added/updated
src/db/metric-stream-replica-identity.integration.test.ts, src/db/migrate.integration.test.ts
New integration test applies 0007 then 0009 against a TimescaleDB instance to assert replica identity, chunk compression behavior, id nullability/backfill, and final composite PK. migrate.integration.test.ts updated to assert pg_class.relreplident = 'f', validate NOT NULL and composite PKs (including oauth_token).
Deleted test
src/db/metric-stream-primary-key.integration.test.ts
Removed the previous single-migration integration test that validated primary key creation and replica identity; replaced by the new multi-step integration test.
Unit test
src/db/migrate.test.ts
Added Vitest case that mocks a migration file to verify statement splitting and marker comment handling; ensures backfill/chunk queries are not executed during partial runs.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as Migration Runner
    participant DB as Postgres DB
    participant Chunks as TimescaleDB Chunks

    Runner->>DB: apply 0007_metric_stream_primary_key.sql
    DB-->>DB: ALTER TABLE add column `id` DEFAULT gen_random_uuid()
    DB-->>DB: set relreplident = 'f' (replica identity full)

    Runner->>DB: apply 0009_metric_stream_id_not_null_primary_key.sql
    DB-->>DB: CREATE PROCEDURE backfill_metric_stream_ids(batch_size)
    Runner->>DB: CALL backfill_metric_stream_ids()
    DB->>DB: build temp table of chunk ranges
    loop per chunk
        DB->>Chunks: optionally decompress chunk (if compressed)
        DB->>DB: UPDATE rows in chunk WHERE id IS NULL (batched)
        DB->>Chunks: optionally recompress chunk
    end
    DB-->>DB: VERIFY no NULL ids remain
    DB-->>DB: ALTER TABLE SET id NOT NULL
    DB-->>DB: CREATE PRIMARY KEY (id, recorded_at)

    Runner->>DB: apply 0010_oauth_token_primary_key.sql
    DB-->>DB: IF no PK on oauth_token THEN add constraint using existing index
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main change: adding a metric stream ID primary key migration with supporting infrastructure for chunk-aware backfill and constraint management.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 Asherlc/metric-stream-id-pr

Review rate limit: 8/10 reviews remaining, refill in 6 minutes and 18 seconds.

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

@github-actions

github-actions Bot commented May 1, 2026 •

Copy link
Copy Markdown
Contributor

Storybook previews for b64cc286 are ready:

This comment updates automatically on each PR push.

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

🧹 Nitpick comments (1)
src/db/metric-stream-replica-identity.integration.test.ts (1)

152-176: ⚡ Quick win

Assert the chunk is still compressed after 0009.

Right now this only proves that a compressed chunk exists before the backfill runs. If 0009 decompresses a chunk and fails to restore its prior state, this test still passes. Add the same timescaledb_information.chunks assertion after the second runMigrations(...) call so the compressed-chunk scenario is actually covered.

Suggested assertion
       const primaryKeyMigrationCount = await runMigrations(connectionString, tmpDir);
       expect(primaryKeyMigrationCount).toBe(1);
 
+      const recompressedChunkResult = await client.query<{ compressed_chunk_count: string }>(`
+        SELECT count(*) AS compressed_chunk_count
+        FROM timescaledb_information.chunks
+        WHERE hypertable_schema = 'fitness'
+          AND hypertable_name = 'metric_stream'
+          AND is_compressed
+      `);
+      expect(recompressedChunkResult.rows).toEqual([{ compressed_chunk_count: "1" }]);
+
       const backfilledResult = await client.query<{ missing_id_count: string }>(
         "SELECT count(*) AS missing_id_count FROM fitness.metric_stream WHERE id IS NULL",
       );

As per coding guidelines, "TDD: Write tests first, then implement. When fixing bugs, write a failing test that reproduces the bug before writing the fix."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/metric-stream-replica-identity.integration.test.ts` around lines 152 -
176, The test currently only checks timescaledb_information.chunks before
running the primary-key migration; after calling runMigrations(connectionString,
tmpDir) the test should re-query timescaledb_information.chunks to assert the
compressed chunk still exists (e.g., repeat the client.query SELECT count(*) ...
WHERE is_compressed and expect the count to remain "1") so that
runMigrations/0009_metric_stream_id_not_null_primary_key.sql does not
inadvertently decompress chunks; place this new assertion immediately after the
primaryKeyMigrationCount(expect) block and before verifying no NULL ids.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/0009_metric_stream_id_not_null_primary_key.sql`:
- Around line 43-50: The current temp table metric_stream_backfill_chunks must
capture each chunk's original compression state from
timescaledb_information.chunks (e.g., add is_compressed boolean) and use that
when deciding to recompress; modify the SELECT that builds
metric_stream_backfill_chunks to include is_compressed, ensure the migration
calls decompress_chunk(...) for all chunks to allow backfill but only calls
compress_chunk(...) for rows where is_compressed = true AND
should_compress_after is true, so chunks that were originally uncompressed
remain uncompressed and originally compressed chunks are restored if old enough.
- Around line 177-178: The direct ALTER TABLE metric_stream ALTER COLUMN id SET
NOT NULL will take an ACCESS EXCLUSIVE lock and perform a full scan; instead add
a NOT VALID check constraint, validate it, then set NOT NULL. Concretely: add a
constraint like metric_stream_id_not_null_chk as CHECK (id IS NOT NULL) NOT
VALID on table metric_stream, run ALTER TABLE metric_stream VALIDATE CONSTRAINT
metric_stream_id_not_null_chk to ensure there are no NULLs (this avoids the full
scan under exclusive lock), and only after validation run ALTER TABLE
metric_stream ALTER COLUMN id SET NOT NULL and optionally DROP CONSTRAINT
metric_stream_id_not_null_chk if you don’t want the duplicate constraint.

---

Nitpick comments:
In `@src/db/metric-stream-replica-identity.integration.test.ts`:
- Around line 152-176: The test currently only checks
timescaledb_information.chunks before running the primary-key migration; after
calling runMigrations(connectionString, tmpDir) the test should re-query
timescaledb_information.chunks to assert the compressed chunk still exists
(e.g., repeat the client.query SELECT count(*) ... WHERE is_compressed and
expect the count to remain "1") so that
runMigrations/0009_metric_stream_id_not_null_primary_key.sql does not
inadvertently decompress chunks; place this new assertion immediately after the
primaryKeyMigrationCount(expect) block and before verifying no NULL ids.
🪄 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: 2a754879-a160-42ca-b54e-f8bbd5f5426a

📥 Commits

Reviewing files that changed from the base of the PR and between 7af8b2f and c39b591.

📒 Files selected for processing (7)
  • drizzle/0007_metric_stream_primary_key.sql
  • drizzle/0009_metric_stream_id_not_null_primary_key.sql
  • drizzle/0010_oauth_token_primary_key.sql
  • src/db/metric-stream-primary-key.integration.test.ts
  • src/db/metric-stream-replica-identity.integration.test.ts
  • src/db/migrate.integration.test.ts
  • src/db/migrate.test.ts
💤 Files with no reviewable changes (2)
  • src/db/metric-stream-primary-key.integration.test.ts
  • drizzle/0007_metric_stream_primary_key.sql

Comment thread drizzle/0009_metric_stream_id_not_null_primary_key.sql
Comment thread drizzle/0009_metric_stream_id_not_null_primary_key.sql Outdated

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

Updates the database migration sequence to safely introduce a primary key for the TimescaleDB fitness.metric_stream hypertable by separating replica identity changes from a chunk-aware ID backfill, and adds a primary key to fitness.oauth_token using an existing unique index.

Changes:

  • Split metric_stream work into: (1) add id default + REPLICA IDENTITY FULL, then (2) chunk-aware backfill + enforce id NOT NULL + add (id, recorded_at) primary key.
  • Add oauth_token composite primary key via existing (user_id, provider_id) unique index.
  • Update/replace integration tests to cover compressed chunks, replica identity behavior, and missing primary keys.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/db/migrate.test.ts Adds a unit test to ensure migration parsing/execution doesn’t apply custom marker “hooks”.
src/db/migrate.integration.test.ts Updates assertions for metric_stream replica identity + PK columns and adds coverage for oauth_token PK.
src/db/metric-stream-replica-identity.integration.test.ts New end-to-end repro that validates the two-step metric_stream migration behavior, including compressed chunks.
src/db/metric-stream-primary-key.integration.test.ts Removes the older integration test in favor of the more complete new repro.
drizzle/0007_metric_stream_primary_key.sql Narrows the earlier migration to only set default id + REPLICA IDENTITY FULL (no backfill/PK yet).
drizzle/0009_metric_stream_id_not_null_primary_key.sql Adds the chunk-aware backfill procedure, enforces id NOT NULL, and creates the hypertable-compatible PK.
drizzle/0010_oauth_token_primary_key.sql Adds oauth_token PK using the existing unique index and enforces user_id NOT NULL.

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

Comment thread src/db/metric-stream-replica-identity.integration.test.ts Outdated
Comment thread drizzle/0010_oauth_token_primary_key.sql Outdated

@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 (1)
src/db/metric-stream-replica-identity.integration.test.ts (1)

156-199: ⚡ Quick win

Assert compression state per chunk, not just the total count.

compressed_chunk_count = "1" still passes if 0009 decompresses the original chunk and recompresses a different one. Snapshot chunk_name/is_compressed before applying 0009 and compare the same ordered rows afterward so this test catches chunk-level compression drift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/metric-stream-replica-identity.integration.test.ts` around lines 156 -
199, Snapshot the per-chunk compression state (select chunk_name and
is_compressed ordered deterministically) before applying migration and store it
(e.g., replace chunkStateResult with rows containing chunk_name and
is_compressed from the timescaledb_information.chunks query), then after
runMigrations (replace finalChunkStateResult assertion) run the same ordered
query and assert the two row arrays are equal; reference the existing queries
around chunkStateResult and finalChunkStateResult and keep the filtering WHERE
hypertable_schema = 'fitness' AND hypertable_name = 'metric_stream' and ordering
(e.g., ORDER BY chunk_name) to ensure chunk-level compression drift is detected.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/0009_metric_stream_id_not_null_primary_key.sql`:
- Around line 25-28: The procedure's main BEGIN lacks an EXCEPTION handler so
session settings and chunk state may be left altered on failure; add an
EXCEPTION WHEN OTHERS block around the main BEGIN...END that restores prior
session configs (reset statement_timeout, lock_timeout,
timescaledb.max_tuples_decompressed_per_dml_transaction and
session_replication_role using the values saved earlier) and, if
should_recompress is true and current_chunk_regclass was decompressed, call
compress_chunk(current_chunk_regclass) (or the equivalent recompression command)
to recompress the chunk, then RAISE the exception to propagate it; reference the
existing variables and calls like current_chunk_regclass, should_recompress,
decompress_chunk, and the saved config variables to implement the
restore/recompress logic.

---

Nitpick comments:
In `@src/db/metric-stream-replica-identity.integration.test.ts`:
- Around line 156-199: Snapshot the per-chunk compression state (select
chunk_name and is_compressed ordered deterministically) before applying
migration and store it (e.g., replace chunkStateResult with rows containing
chunk_name and is_compressed from the timescaledb_information.chunks query),
then after runMigrations (replace finalChunkStateResult assertion) run the same
ordered query and assert the two row arrays are equal; reference the existing
queries around chunkStateResult and finalChunkStateResult and keep the filtering
WHERE hypertable_schema = 'fitness' AND hypertable_name = 'metric_stream' and
ordering (e.g., ORDER BY chunk_name) to ensure chunk-level compression drift is
detected.
🪄 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: 1b1c778b-fde3-4e21-9714-db3c79b84db9

📥 Commits

Reviewing files that changed from the base of the PR and between c39b591 and 9327bb6.

📒 Files selected for processing (4)
  • drizzle/0009_metric_stream_id_not_null_primary_key.sql
  • drizzle/0010_oauth_token_primary_key.sql
  • src/db/metric-stream-replica-identity.integration.test.ts
  • src/db/migrate.integration.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • drizzle/0010_oauth_token_primary_key.sql
  • src/db/migrate.integration.test.ts

Comment thread drizzle/0009_metric_stream_id_not_null_primary_key.sql
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Review app is ready:

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

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