archestra-dev-interactions-migrations
GitHub指导在修改高并发写入的大表(如interactions)时,如何安全执行数据库迁移。涵盖安全与高风险操作区分、禁止事务内重建索引规则,以及上线前对预发环境的审计流程,防止锁表导致服务中断。
Trigger Scenarios
Install
npx skills add archestra-ai/archestra --skill archestra-dev-interactions-migrations -g -y
SKILL.md
Frontmatter
{
"name": "archestra-dev-interactions-migrations",
"description": "Use BEFORE writing or running any Drizzle migration that touches the `interactions` table (or any other very large, write-hot table). The interactions table is the platform's biggest, append-heavy table — every LLM proxy call writes a row — so a careless migration can take a write-blocking lock and stall the proxy. Covers which operations are safe vs table-rewriting\/lock-taking, the \"never rebuild an index in a transactional migration\" rule, and a read-only audit procedure against the GKE staging database to size the risk first."
}
Migrations against the interactions table
The interactions table is special: it is the largest table in the platform and
is on the LLM proxy's hot write path — every proxied LLM call inserts a row. A
migration that takes a strong lock on it, even briefly, blocks those inserts, so
the proxy cannot record interactions until the migration finishes. On a large
table a "quick" CREATE INDEX can hold that lock for minutes.
Treat any schema change to interactions as production-risk work. The same rules
apply to any other very large, write-hot table.
Safe vs risky operations
Safe (fast, metadata-only, no table rewrite in PostgreSQL 11+):
ADD COLUMN ... DEFAULT <constant> NOT NULL— the default is stored as metadata; existing rows are not rewritten. This is instant regardless of table size. (The billing_mode column was added this way.)ADD COLUMNnullable, with no default.DROP DEFAULT,SET DEFAULT <constant>, renaming a column.
Risky (rewrites the whole table or takes a write-blocking lock — scales with table size):
ADD COLUMN ... DEFAULT <volatile expr>(e.g.now(),gen_random_uuid()) — rewrites every row.ALTER COLUMN ... TYPE ...— usually rewrites the table.SET NOT NULLon an existing column — full scan to validate.CREATE INDEX/DROP INDEX(non-concurrent) — this is the most common trap. A plainCREATE INDEXtakes aSHARElock that blocks writes for the entire build; adding a column to an existing covering index means aDROP INDEX+CREATE INDEXrebuild.
The index rule
Never add, drop, or rebuild an index on interactions inside a Drizzle
migration. Drizzle runs each migration in a single transaction, and
CREATE INDEX CONCURRENTLY / DROP INDEX CONCURRENTLY cannot run inside a
transaction — so the only thing a generated migration can emit is the blocking,
non-concurrent form.
Instead:
- Keep the Drizzle schema's index definition matching what is actually deployed,
so
pnpm db:generatedoes not emit an index change. If you need a new index for a query, decide whether the query can tolerate a heap fetch instead — for an analytics query (not the hot path) it usually can. - If the index is genuinely needed, apply it out of band as an ops step with
CREATE INDEX CONCURRENTLY(andDROP INDEX CONCURRENTLYfor the old one) during a maintenance window, then update the schema to match.CONCURRENTLYbuilds without blocking writes, at the cost of a slower build and a second table scan.
The migration linter (pnpm --dir backend check:migrations) flags DROP INDEX
as an error and non-concurrent CREATE INDEX as a warning for exactly this
reason. If it fires on an interactions migration, stop and rework the change —
do not just add the allow-breaking marker.
Audit the table on staging before you ship
Before merging a migration that touches interactions, size the real table on
the GKE staging database so you know the blast radius. This is read-only —
never run the migration DDL by hand against staging or production; migrations
deploy through the normal pipeline.
Access is via GCP/GKE IAM (managed separately from this repo), so the commands below grant nothing on their own.
-
Switch kubectl to the GKE staging context:
kubectl config get-contexts -o name | grep archestra-staging # e.g. gke_<project>_us-central1-a_archestra-staging kubectl config use-context <that-context> -
Find the Postgres pod (namespace
archestra, containerpostgresql):kubectl get pods -n archestra | grep postgresql # archestra-platform-postgresql-0 -
Open a read-only psql session (use the app credentials already in the pod's environment; do not export secrets):
kubectl exec -it -n archestra archestra-platform-postgresql-0 -c postgresql \ -- bash -lc 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U "$POSTGRES_USER" -d "$POSTGRES_USER"' -
Run the audit queries (all read-only):
-- PostgreSQL version. Metadata-only ADD COLUMN ... DEFAULT needs 11+. SELECT version(); -- Fast row estimate. NEVER run count(*) on this table — it scans everything. SELECT reltuples::bigint AS est_rows, relpages FROM pg_class WHERE relname = 'interactions'; -- Heap / TOAST / index sizes. SELECT pg_size_pretty(pg_total_relation_size('interactions')) AS total, pg_size_pretty(pg_relation_size('interactions')) AS heap, pg_size_pretty(pg_indexes_size('interactions')) AS indexes; -- Per-index size — a non-concurrent rebuild is at least this expensive. SELECT indexrelname, pg_size_pretty(pg_relation_size(indexrelid)) AS size FROM pg_stat_user_indexes WHERE relname = 'interactions' ORDER BY pg_relation_size(indexrelid) DESC; -- Long-running transactions. A CREATE INDEX waits behind these AND, once it -- starts, blocks writes until it finishes — so know what's open first. SELECT pid, now() - xact_start AS xact_age, state, left(query, 80) AS query FROM pg_stat_activity WHERE xact_start IS NOT NULL AND pid <> pg_backend_pid() ORDER BY xact_start LIMIT 10; -
Read the numbers:
- Metadata-only changes (safe
ADD COLUMN) are effectively instant no matter how big the table is — ship them normally. - A table rewrite or a non-concurrent index build scales with heap/index size. As a rough order of magnitude, an index build reads the whole table, sorts, and writes the index — expect it to be at least as slow as a full scan of the heap, and it holds the write lock the entire time. If that is more than a couple of seconds of estimated build time, do not do it in a transactional migration (see "The index rule").
- Metadata-only changes (safe
When you switch away, restore your previous kubectl context
(kubectl config use-context <previous>).
See also
archestra-dev-migrations— the general migration flow (pnpm db:generate,drizzle-kit check,check:migrations, data migrations, conflict resolution).
Version History
- 3053975 Current 2026-08-12 09:04


