Skip to content

feat: catalog_valuations — persisted valuation history (chat#1889 row 15) - #52

Merged
sweetmantech merged 1 commit into
mainfrom
feat/catalog-valuations-table
Jul 29, 2026
Merged

feat: catalog_valuations — persisted valuation history (chat#1889 row 15)#52
sweetmantech merged 1 commit into
mainfrom
feat/catalog-valuations-table

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Database leg of row 15, the keystone on chat#1889. Merge order: docs#279 → this → api#TBD.

catalog_valuations: one row per band computation — catalog_id (FK CASCADE), low/mid/high (USD), measured_song_count, total_streams, measured_at — with a (catalog_id, measured_at DESC) index matching the only read shape (latest-first per catalog). History table by design, never columns on catalogs: the whole point is two numbers to compare.

Writers land in the api PR: runValuationHandler (always) + the measurements read path (at most one row per catalog per day). Schema-only migration — no data touched; trivially fresh-env safe.

🤖 Generated with Claude Code


Summary by cubic

Add persisted valuation history per catalog via a new catalog_valuations table so we can compare values over time (chat#1889, row 15). Optimized for latest‑first reads; writers will be added in the API follow-up.

  • New Features

    • Adds catalog_valuations with: catalog_id, low/mid/high (USD), measured_song_count, total_streams, measured_at, created_at.
    • FK to catalogs with ON DELETE CASCADE.
    • Index on (catalog_id, measured_at DESC) for efficient latest-first queries.
  • Migration

    • Schema-only; no data touched and safe on fresh environments.

Written for commit 76c03c0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added persistent valuation history for catalogs.
    • Valuation records now capture low, mid, and high estimates, song counts, total streams, and measurement timestamps.
    • Catalog valuation history is automatically cleaned up when its associated catalog is deleted.

chat#1889 row 15 (keystone). One row per band computation; read is
always latest-first per catalog, hence the (catalog_id, measured_at
DESC) index. FK CASCADE to catalogs: a valuation row is meaningless
without its catalog. Contract: docs#279.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 29, 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.

@supabase

supabase Bot commented Jul 29, 2026

Copy link
Copy Markdown

Updates to Preview Branch (feat/catalog-valuations-table) ↗︎

Deployments Status Updated
Database Wed, 29 Jul 2026 19:30:34 UTC
Services Wed, 29 Jul 2026 19:30:34 UTC
APIs Wed, 29 Jul 2026 19:30:34 UTC

Tasks are run on every commit but only new migration files are pushed.
Close and reopen this PR if you want to apply changes from existing seed or migration files.

Tasks Status Updated
Configurations Wed, 29 Jul 2026 19:30:42 UTC
Migrations Wed, 29 Jul 2026 19:30:47 UTC
Seeding Wed, 29 Jul 2026 19:30:47 UTC
Edge Functions Wed, 29 Jul 2026 19:30:47 UTC

View logs for this Workflow Run ↗︎.
Learn more about Supabase for Git ↗︎.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A Supabase migration creates public.catalog_valuations to persist catalog valuation history, including valuation bands, measurement metadata, timestamps, catalog ownership, and an index for retrieving recent valuations.

Changes

Catalog valuation history

Layer / File(s) Summary
Valuation history schema
supabase/migrations/20260729230000_create_catalog_valuations.sql
Creates the valuation history table with UUID identity, valuation bands, measurement counters, timestamp defaults, a cascading catalog foreign key, and an index on catalog and measurement time.

Estimated code review effort: 1 (Trivial) | ~5 minutes

🚥 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 title clearly describes the main change: adding persisted valuation history for catalog_valuations.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/catalog-valuations-table

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.

@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

🤖 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 `@supabase/migrations/20260729230000_create_catalog_valuations.sql`:
- Around line 21-31: Add table-level CHECK constraints to
public.catalog_valuations ensuring low, mid, and high are non-negative and
ordered low <= mid <= high, and measured_song_count and total_streams are
non-negative. Keep the existing columns and defaults unchanged.
- Around line 21-31: Update the catalog_valuations table definition to establish
its intended API access path, matching the neighboring catalog tables: either
grant the required privileges to the service-role endpoint or enable RLS and add
policies permitting authenticated reads for GET
/api/catalogs/{catalogId}/valuations. Ensure service-role writes remain
permitted and the catalog_id scope is enforced consistently with existing
policies.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f51c220b-270a-4da6-9cc3-78f1221a1057

📥 Commits

Reviewing files that changed from the base of the PR and between 27c2c90 and 76c03c0.

📒 Files selected for processing (1)
  • supabase/migrations/20260729230000_create_catalog_valuations.sql

Comment on lines +21 to +31
CREATE TABLE IF NOT EXISTS public.catalog_valuations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
catalog_id UUID NOT NULL REFERENCES public.catalogs(id) ON DELETE CASCADE,
low NUMERIC NOT NULL, -- band low, USD
mid NUMERIC NOT NULL, -- band midpoint, USD
high NUMERIC NOT NULL, -- band high, USD
measured_song_count INTEGER NOT NULL, -- songs measured in the underlying capture
total_streams BIGINT NOT NULL, -- whole-catalog lifetime streams at measurement
measured_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Enforce valuation invariants in the schema.

NOT NULL still permits negative counts/streams and invalid bands such as low > mid or mid > high. Add table-level CHECK constraints so every writer preserves the history contract.

Proposed fix
     measured_at         TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
-    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
+    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+    CONSTRAINT catalog_valuations_band_order_chk
+      CHECK (low >= 0 AND low <= mid AND mid <= high),
+    CONSTRAINT catalog_valuations_song_count_chk
+      CHECK (measured_song_count >= 0),
+    CONSTRAINT catalog_valuations_streams_chk
+      CHECK (total_streams >= 0)
 );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE TABLE IF NOT EXISTS public.catalog_valuations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
catalog_id UUID NOT NULL REFERENCES public.catalogs(id) ON DELETE CASCADE,
low NUMERIC NOT NULL, -- band low, USD
mid NUMERIC NOT NULL, -- band midpoint, USD
high NUMERIC NOT NULL, -- band high, USD
measured_song_count INTEGER NOT NULL, -- songs measured in the underlying capture
total_streams BIGINT NOT NULL, -- whole-catalog lifetime streams at measurement
measured_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS public.catalog_valuations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
catalog_id UUID NOT NULL REFERENCES public.catalogs(id) ON DELETE CASCADE,
low NUMERIC NOT NULL, -- band low, USD
mid NUMERIC NOT NULL, -- band midpoint, USD
high NUMERIC NOT NULL, -- band high, USD
measured_song_count INTEGER NOT NULL, -- songs measured in the underlying capture
total_streams BIGINT NOT NULL, -- whole-catalog lifetime streams at measurement
measured_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
CONSTRAINT catalog_valuations_band_order_chk
CHECK (low >= 0 AND low <= mid AND mid <= high),
CONSTRAINT catalog_valuations_song_count_chk
CHECK (measured_song_count >= 0),
CONSTRAINT catalog_valuations_streams_chk
CHECK (total_streams >= 0)
);
🤖 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 `@supabase/migrations/20260729230000_create_catalog_valuations.sql` around
lines 21 - 31, Add table-level CHECK constraints to public.catalog_valuations
ensuring low, mid, and high are non-negative and ordered low <= mid <= high, and
measured_song_count and total_streams are non-negative. Keep the existing
columns and defaults unchanged.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## migration file excerpt"
if [ -f supabase/migrations/20260729230000_create_catalog_valuations.sql ]; then
  cat -n supabase/migrations/20260729230000_create_catalog_valuations.sql
else
  echo "migration file not found"
fi

echo
echo "## related catalog policy/RLS references"
rg -n "catalog_valuations|catalogs|RLS|enable row level security|ALTER TABLE .* ENABLE ROW LEVEL SECURITY|CREATE POLICY|GRANT .* catalog_valuations|pg_ls_rel_acl|supabase/functions|auth/user" -S supabase . -g '!node_modules' -g '!dist' -g '!build' | head -200

echo
echo "## SQL policy creation / grants candidates"
fd -e sql . supabase public supabase -x sh -c 'echo "--- $1"; rg -n "catalog_valuations|catalogs|ENABLE ROW LEVEL SECURITY|ALTER TABLE .* ENABLE ROW LEVEL SECURITY|CREATE POLICY|GRANT .* catalog|revoke all|role anon|role authenticated" "{}" -S' sh {}

echo
echo "## git diff stat/name/status"
git diff --stat || true
git diff --name-only || true

Repository: recoupable/database

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## catalogs migration"
cat -n supabase/migrations/20251005212508_create_catalogs_table.sql

echo
echo "## catalog-songs/measurements related migrations excerpts"
cat -n supabase/migrations/20260707120000_catalog_measurements_read_functions.sql
echo "---"
if [ -f supabase/migrations/20251005214038_create_catalog_songs_table.sql ]; then cat -n supabase/migrations/20251005214038_create_catalog_songs_table.sql; fi
echo "---"
if [ -f supabase/migrations/20251005210048_create_songs_table.sql ]; then sed -n '1,80p' supabase/migrations/20251005210048_create_songs_table.sql | cat -n; fi

echo
echo "## valuations route/API references"
rg -n "valuations|/catalogs/.*/valuations|catalogValuations|getCatalogValuations|runValuationHandler|catalog_id|catalogValuation" -S . \
  -g '!node_modules' -g '!dist' -g '!build' | head -250

echo
echo "## table grants and RLS statements for public catalog tables (targeted)"
python3 - <<'PY'
from pathlib import Path
needle = ['catalogs', 'catalog_songs', 'catalog_valuations', 'account_catalogs']
for p in Path('supabase/migrations').glob('*.sql'):
    s = p.read_text(errors='ignore')
    hits = [key for key in needle if key in s.lower()]
    if not hits:
        continue
    stmts = []
    for line in s.splitlines():
        l=line.lower().strip()
        if any(key in l for key in hits):
            stmts.append(line)
    print(f'--- {p}')
    for i, line in enumerate(s.splitlines(), 1):
        if any(key in line.lower() for key in hits) or any(x in l_for_line for x in ['grant ', 'alter table', 'create policy', 'enable row level security', 'revoke ']):
            print(f'{i:04d}: {line}')
    print()
PY

Repository: recoupable/database

Length of output: 11784


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## account_catalogs migration excerpt"
sed -n '1,120p' supabase/migrations/20251005214926_create_account_catalogs_table.sql | cat -n

echo
echo "## valuations references across repo"
rg -n "catalog_valuation|catalogValuation|/valuations|valuations" -S . \
  -g '!node_modules' -g '!dist' -g '!build' -g '!supabase/migrations/20260729230000_create_catalog_valuations.sql' | head -200

echo
echo "## all catalog_valuations references"
rg -n "catalog_valuations|catalog_valuation" -S . \
  -g '!node_modules' -g '!dist' -g '!build' | head -250

Repository: recoupable/database

Length of output: 3595


Grant catalog_valuations to the service-role endpoint or add RLS policies for authenticated access.

This table is documented as read by GET /api/catalogs/{catalogId}/valuations, but unlike the neighbor catalog tables, it has no grants/RLS. As written, the service-role writes can run and PostgREST queries will fail with the “new row violates row-level security policy” check on access-controlled tables, so add the intended grant/policy path here.

🤖 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 `@supabase/migrations/20260729230000_create_catalog_valuations.sql` around
lines 21 - 31, Update the catalog_valuations table definition to establish its
intended API access path, matching the neighboring catalog tables: either grant
the required privileges to the service-role endpoint or enable RLS and add
policies permitting authenticated reads for GET
/api/catalogs/{catalogId}/valuations. Ensure service-role writes remain
permitted and the catalog_id scope is enforced consistently with existing
policies.

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 1 file

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="supabase/migrations/20260729230000_create_catalog_valuations.sql">

<violation number="1" location="supabase/migrations/20260729230000_create_catalog_valuations.sql:30">
P2: The table only enforces NOT NULL on low/mid/high, measured_song_count, and total_streams. Nothing stops a writer from inserting negative counts/streams or an invalid band where low > mid or mid > high. Consider adding CHECK constraints (e.g. CHECK (low >= 0 AND low <= mid AND mid <= high), CHECK (measured_song_count >= 0), CHECK (total_streams >= 0)) so the schema itself preserves the valuation history contract.</violation>

<violation number="2" location="supabase/migrations/20260729230000_create_catalog_valuations.sql:35">
P1: Catalog valuation history can be read or modified through the public data API without catalog-level authorization. Enable RLS here (the API's service role still bypasses it), matching `catalogs` and the measurement store.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


-- The read is always "latest rows for one catalog".
CREATE INDEX IF NOT EXISTS catalog_valuations_catalog_measured_idx
ON public.catalog_valuations (catalog_id, measured_at DESC);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Catalog valuation history can be read or modified through the public data API without catalog-level authorization. Enable RLS here (the API's service role still bypasses it), matching catalogs and the measurement store.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260729230000_create_catalog_valuations.sql, line 35:

<comment>Catalog valuation history can be read or modified through the public data API without catalog-level authorization. Enable RLS here (the API's service role still bypasses it), matching `catalogs` and the measurement store.</comment>

<file context>
@@ -0,0 +1,35 @@
+
+-- The read is always "latest rows for one catalog".
+CREATE INDEX IF NOT EXISTS catalog_valuations_catalog_measured_idx
+  ON public.catalog_valuations (catalog_id, measured_at DESC);
</file context>

measured_song_count INTEGER NOT NULL, -- songs measured in the underlying capture
total_streams BIGINT NOT NULL, -- whole-catalog lifetime streams at measurement
measured_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The table only enforces NOT NULL on low/mid/high, measured_song_count, and total_streams. Nothing stops a writer from inserting negative counts/streams or an invalid band where low > mid or mid > high. Consider adding CHECK constraints (e.g. CHECK (low >= 0 AND low <= mid AND mid <= high), CHECK (measured_song_count >= 0), CHECK (total_streams >= 0)) so the schema itself preserves the valuation history contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/migrations/20260729230000_create_catalog_valuations.sql, line 30:

<comment>The table only enforces NOT NULL on low/mid/high, measured_song_count, and total_streams. Nothing stops a writer from inserting negative counts/streams or an invalid band where low > mid or mid > high. Consider adding CHECK constraints (e.g. CHECK (low >= 0 AND low <= mid AND mid <= high), CHECK (measured_song_count >= 0), CHECK (total_streams >= 0)) so the schema itself preserves the valuation history contract.</comment>

<file context>
@@ -0,0 +1,35 @@
+    measured_song_count INTEGER  NOT NULL,  -- songs measured in the underlying capture
+    total_streams       BIGINT   NOT NULL,  -- whole-catalog lifetime streams at measurement
+    measured_at         TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
+    created_at          TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()
+);
+
</file context>

@sweetmantech

Copy link
Copy Markdown
Contributor Author

Pre-merge verification (2026-07-29)

Check Method Result
Fresh-environment compatibility Supabase Preview check on this PR ✅ SUCCESS (no out-of-band tables, no hardcoded ids — pure CREATE TABLE IF NOT EXISTS + index)
Executes on the intended target Exact file bytes (fetched from the PR head, byte-identical to local, 1,995 bytes) run against prod in BEGIN…ROLLBACK, followed by a probe INSERT selecting a real catalog id ✅ ran end-to-end without error; pg_indexes inside the transaction showed catalog_valuations_pkey + catalog_valuations_catalog_measured_idx
Rollback left prod untouched to_regclass('public.catalog_valuations') after ROLLBACK null — table does not exist

No data manipulation in this migration; on merge the Supabase GitHub integration applies it to prod. Post-merge check will be the same to_regclass + index probe, this time expecting the table to exist.

Merge order: docs#279 (✅ merged) → thisapi#795.

@sweetmantech
sweetmantech merged commit 191c67b into main Jul 29, 2026
3 checks passed
@sweetmantech

Copy link
Copy Markdown
Contributor Author

Post-merge verification (2026-07-29, prod)

Supabase check on merge commit 191c67b: success. Probed prod directly:

Assertion Result
to_regclass('public.catalog_valuations') catalog_valuations
Indexes catalog_valuations_pkey + catalog_valuations_catalog_measured_idx
FK delete rule (catalog_valuations_catalog_id_fkey) c (CASCADE)
schema_migrations 20260729230000 present — no version drift
Row count 0 (expected — writers ship in api#795)

Matches the pre-merge dry-run exactly. Chain unblocked: api#795 is next.

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.

1 participant