Skip to content

feat(billing): add microdollar_usage_daily rollup with dual-write#3270

Open
RSO wants to merge 7 commits into
mainfrom
pewter-result
Open

feat(billing): add microdollar_usage_daily rollup with dual-write#3270
RSO wants to merge 7 commits into
mainfrom
pewter-result

Conversation

@RSO
Copy link
Copy Markdown
Contributor

@RSO RSO commented May 15, 2026

Summary

PR 1 of 2 to eliminate the 3-month scan of the 800M-row microdollar_usage table that powers kiloPass.getAverageMonthlyUsageLast3Months and currently accounts for ~42% of read-replica execution time.

This PR adds the rollup infrastructure but does not switch the read path:

  • New microdollar_usage_daily table with a row per (kilo_user_id, organization_id, usage_date) and two partial unique indexes (one for organization_id IS NULL, one for IS NOT NULL). Modeled on exa_monthly_usage.
  • Extended insertUsageAndMetadataWithBalanceUpdate (apps/web/src/lib/ai-gateway/processUsage.ts) with a new CTE that upserts the matching daily row, atomic with the existing microdollar_usage insert in the same single SQL statement. Zero-cost rows are skipped via WHERE ${cost} <> 0.
  • A test helper insertMicrodollarUsageWithDailyRollup in apps/web/src/tests/helpers/microdollar-usage.helper.ts for tests that bypass the production hot path and write to microdollar_usage directly. (PR 2 tests will use this; existing tests are left alone since they don't read from the rollup yet.)
  • New tests for the dual-write covering personal scope, increments on the same day, separate org-scoped rollup, and the zero-cost no-op.

After deploy, every new microdollar_usage row is mirrored into microdollar_usage_daily. The historical backfill and the read-path switch are tracked in the implementation plan and will land in PR 2.

Plan: .kilo/plans/1778835512309-hidden-sailor.md.

Verification

  • Applied the migration locally with pnpm drizzle migrate.
  • Inserted a personal-scope usage row through the production hot path and confirmed a corresponding microdollar_usage_daily row appeared with the expected total. Inserted a second row on the same day and confirmed the row's total_cost_microdollars incremented (not duplicated). Inserted an org-scoped row and confirmed it created a separate row hitting the org partial unique index, leaving the personal row unchanged. Inserted a zero-cost row and confirmed the rollup was untouched.
  • Confirmed a row with created_at set explicitly to a UTC date lands in microdollar_usage_daily.usage_date as the same calendar day.

Visual Changes

N/A — backend / schema only.

Reviewer Notes

  • The dual-write CTE picks the matching partial-unique-index ON CONFLICT target with a JS-level branch on whether organization_id is null. PostgreSQL only allows a single ON CONFLICT clause per statement and the two partial indexes have different column lists, so this branch is required.
  • microdollar_usage_daily stores both personal and org-scoped rows in one table to mirror exa_monthly_usage. Only the personal partial index is exercised by the immediate consumer in PR 2; the org-scoped rows are written but unused for now.
  • The new table contains no PII (only kilo_user_id, organization_id, usage_date, aggregated cost). Per softDeleteUser policy, microdollar_usage itself is retained on soft-delete as a billing record (apps/web/src/lib/user.ts retention list); this derived rollup follows the same retention. No softDeleteUser change required.
  • Risk on the hot path: the added CTE is a single INSERT ... ON CONFLICT DO UPDATE against a small, tightly-indexed table. It shares the same transaction as the existing microdollar_usage insert and the user-balance UPDATE, so atomicity is preserved with no extra round-trip.
  • The migration was generated via pnpm drizzle generate per repo convention; no hand-written DDL.

RSO added 2 commits May 15, 2026 15:06
Adds a per-(user, organization, day) rollup of microdollar_usage.cost,
maintained atomically alongside the existing CTE-based microdollar_usage
insert in insertUsageAndMetadataWithBalanceUpdate.

This is PR 1 of 2 for replacing the kiloPass.getAverageMonthlyUsageLast3Months
read query, which currently scans 3 months of the 800M-row microdollar_usage
table on every authenticated profile page load (~42% of read-replica time).
PR 1 only adds the table and the dual-write; reads still hit the raw table.
PR 2 will switch the read after the historical backfill runs.
Comment thread apps/web/src/tests/helpers/microdollar-usage.helper.ts
Comment thread packages/db/src/migrations/0131_known_banshee.sql
@kilo-code-bot
Copy link
Copy Markdown
Contributor

kilo-code-bot Bot commented May 15, 2026

Code Review Summary

Status: 2 Issues Found | Recommendation: Address before merge

Executive Summary

Two pre-existing issues remain unaddressed: a redundant dead-code condition in the test helper and a missing trailing newline in the migration file. The new commit only reformats apps/web/tsconfig.json (no logic changes).

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/tests/helpers/microdollar-usage.helper.ts 153 !row.cost || row.cost === 0 — the || row.cost === 0 clause is dead code; !0 is already true. Use row.cost === 0 alone for clarity and alignment with production's WHERE cost <> 0.

SUGGESTION

File Line Issue
packages/db/src/migrations/0131_known_banshee.sql 11 Missing trailing newline at end of file (indicated by \ No newline at end of file in the diff).
Other Observations (not in diff)

microdollar_usage_daily has no created_at column — The table only has updated_at, so there is no record of when a daily rollup row was first created. This is almost certainly intentional (the row is purely a running aggregate), but worth knowing for future observability queries.

$onUpdateFn in schema won't fire for raw SQL CTEsupdated_at's $onUpdateFn(() => sql\now()`)is a Drizzle ORM hook that only fires when Drizzle's.update()builder is used. The CTE explicitly setsupdated_at = NOW()`, which is correct. The hook is effectively a no-op for the hot path here but that's fine.

Files Reviewed (9 files)
  • apps/web/src/lib/ai-gateway/processUsage.ts — no issues
  • apps/web/src/lib/ai-gateway/processUsage.test.ts — no issues
  • apps/web/src/tests/helpers/microdollar-usage.helper.ts — 1 issue (inline comment posted)
  • apps/web/tsconfig.json — no issues (reformatting + removal of explicit moduleResolution: node, which is the default for module: commonjs)
  • packages/db/src/schema.ts — no issues
  • packages/db/src/migrations/0131_known_banshee.sql — 1 issue (inline comment posted)
  • packages/db/src/migrations/meta/0131_snapshot.json — (generated, skipped)
  • packages/db/src/migrations/meta/_journal.json — (generated, skipped)

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-4.6 · 197,390 tokens

Review guidance: REVIEW.md from base branch main

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