This document records which database tables are owned by which schema system in this repo, and how we prevent drift between them. The goal is to ensure no table is silently defined in two ORMs with conflicting types, and to catch regressions in CI via src/__tests__/schema-drift.test.ts.
These tables are SQLite-owned and must be represented in both:
- Drizzle schema (
src/db/schema.ts) and - Raw SQLite migrations (
migrations/*.sql)
Owned tables:
developersapisapi_endpointsschema_versions
These tables are PostgreSQL-owned by Prisma and are represented in:
- Prisma schema (
prisma/schema.prisma) with an explicit@@map("...")
Owned tables:
users(Prisma modelUser @@map("users"))
Some services use pg directly (see src/db.ts) and have their own raw SQL / operational ownership. These tables are not checked by the SQLite drift test.
The Jest drift test (src/__tests__/schema-drift.test.ts) enforces:
- Exact Drizzle table set: Drizzle may only define
developers,apis,api_endpoints,schema_versions - Exact Prisma table set (via
@@map): Prisma may only defineusers - No overlap: a table name may not appear as owned by both ORMs
- SQLite migrations consistency: SQL migrations must not create tables outside the SQLite-owned set, and every created table must exist in Drizzle
- Cross-domain compatibility check:
developers.user_idremains a UUID-shaped string compatible with PrismaUser.id(UUID string)
/src/generated/prisma is intentionally ignored in .gitignore. The drift test validates Prisma ownership and key field types from prisma/schema.prisma directly, so CI doesn’t depend on generated files being present in the repo.
Run:
npm test -- src/__tests__/schema-drift.test.tsMigration: migrations/0011_partition_usage_events.sql
Backfill: scripts/backfill-usage-partitions.ts
usage_events was converted to a Postgres declarative hash-partitioned table with 16 child partitions (usage_events_p0 … usage_events_p15), keyed on developer_id.
A developer_id VARCHAR(255) NOT NULL DEFAULT '' column was added. All new rows must supply the owning developer's ID so the planner can prune to a single partition on every per-developer query.
The old UNIQUE (request_id) constraint was replaced with UNIQUE (request_id, developer_id) because Postgres requires the partition key to be part of every unique / primary-key constraint on a partitioned table. ON CONFLICT (request_id, developer_id) DO UPDATE … in PgUsageEventsRepository.create() and PostgresUsageStore.record() were updated accordingly.
| Index | Columns | Purpose |
|---|---|---|
idx_usage_events_developer_created |
(developer_id, created_at) |
Partition pruning + time-range scans |
idx_usage_events_user_created |
(user_id, created_at) |
Consumer queries |
idx_usage_events_api_created |
(api_id, created_at) |
API revenue aggregation |
- Add
developer_idto existing flat table, backfill fromapis. - Create
usage_events_partitionedparent + 16 child partitions. - Rename: flat table →
usage_events_old, partitioned →usage_events. - Backfill script copies rows
ON CONFLICT (request_id, developer_id) DO NOTHING— safe to re-run.
Queries including WHERE developer_id = $1 hit exactly one partition. Verify with:
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM usage_events WHERE developer_id = 'dev-123' AND created_at > NOW() - INTERVAL '7 days';Expected: Partitions: usage_events_pN (1 out of 16).