db-deprecate-table

Retiring a dead table by moving it to the deprecated schema and clearing every reference in both repos. Use when asked to deprecate or retire a table, when a table is no longer used, or to take a tabl

SKILL.md

GitHub

Deprecate a table

<!-- rules:stamp -->

Rules from common-docs/policies/database-changeover-doctrine.md — edit there, never here (python3 common-docs/meta/scripts/one_home.py --write).

  1. Fix what is actually broken right now.
  2. While doing that, move the changeover forward a little — a few tables at a time, each one complete in every repo.
  3. Database counts mean nothing (Arman, 2026-09-12). There are zero users; all data is test data or his own, and a count only shows how much was tested. Never lead with a number, rank by volume, argue a name or design from row counts, or put counts in a brief as evidence; design for the live-week scale (100x today).
  4. There is no real data and there are no real users (Arman, 2026-09-28). Functionality first: primitive or store capability, then rendering, then editing, then a guard on the code path. Never spend a lane protecting, reconciling or carefully copying existing rows; the data copy is a plain mover step that can run again. Never open with a data census or a preservation gate. Data destruction still stops for confirmation; data fidelity does not.
  5. Never report on graveyard tables, and do not triage public until the canonical tables are right (Arman, 2026-08-12): graveyard is dead and known (deleting it may become an explicit task; until then it never appears in findings, summaries or "worth flagging" lists), public is a queue (canonical-first triage). When he repeats this hint to an agent, the fix is a missing pointer somewhere that agent read: find it and add it.
  6. Focus on what is right, not what is wrong. Learn how the canonical system works when correct (base entity, organization_id, access, associations), so that when a table is named the correct answer is already known. Mention a legacy item only when Arman names it or a task requires it; big dug-up findings are not the small critical fixes he has in mind.<!-- rules:stamp-end -->

Once a table is here it is dead to every database check: findings on deprecated tables are never reported or fixed — canonical-first triage.

Goal: get the table offline and reversible (SET SCHEMA deprecated, never DROP), then erase every reference. Getting it offline is priority #1; reference cleanup follows and must not block the move. Read ../db-change/TOOLKIT.md + ../db-change/SKILL.md first. Project: brsgrqvjdzwihsvnfqkf.

Step 1 — Discover every reference (this is the real work)

🚨 THE ORACLE FIRST — audit.relation_usage(schema, table) (built 2026-08-20, deprecated-sweep postmortem). One call returns EVERY DB-side mention: inbound/outbound FKs, dependent views/matviews recursively (each dependent view joins the closure, so its consumers are found too), triggers on it, every function/procedure body (qualified vs bare matches labeled), RLS policy expressions on other tables, trigger string-arguments (_version_capture tokens, org-inherit args), cron jobs, realtime publications, and the entity/shareable/deprecation registries.

select * from audit.relation_usage('<schema>', '<table>') order by via, kind, ref;

The transitive hop is the whole point: single-hop name searches missed deprecated.prompt_builtins → public.context_menu_unified_view → get_ssr_shell_data (a LIVE session hydrator) in the 2026-08-20 sweep — the table name appears nowhere in the function, only the view's name does. Never substitute a plain pg_proc ilike scan for this call. function_body_bare rows can be same-named objects in another schema — read each; function_body_qualified rows are certain.

Two checks the oracle does not cover:

-- is anything still actually reading it? (needs pg_stat_statements)
select calls, query from pg_stat_statements where query ~* '\m<table>\M' order by calls desc limit 20;

Code (both repos): grep <table> for .from('<table>'), .schema(...).from('<table>'), generated type names, Python model/manager names (aidream/db/models*.py, db/managers/**), package wiring in aidream/package_integration.py, and raw SQL strings (from <table>, SELECT 1 FROM <table> ACL joins in .py/.sql). Also grep for every ref the oracle returned — a dependent view or RPC it surfaces can be called from code by ITS name with the table name appearing nowhere in the repo.

A 0-row table can still be LIVE. Verified: note_shares had 0 rows but was joined by RAG-search ACL (matrx-rag/search.py: SELECT 1 FROM public.note_shares) — deprecating it turned an empty result into a missing-relation error, breaking search. Row count ≠ usage. The query string is what breaks; grep it before you move. (Recovery: alter table deprecated.<t> set schema public — reversible, which is why we never DROP.)

Step 2 — Confirm it's truly dead

If reads remain: repoint or delete those consumers if quick; otherwise deprecate now (reversible) and track the remaining cleanup in the Open items queue of ../common-docs/operations/db-changeover-board.md (the tracker that replaced the deleted docs/db_rebuild/CHANGEOVER_PROGRESS.md). Do not block the move on a long repoint — but never deprecate a table with live, load-bearing reads you haven't accounted for.

Step 3 — Resolve FKs in BOTH directions

Inbound (tables that point AT this one): a cross-schema FK keeps working after the move, so the move won't break — but a dead table shouldn't be referenced. Drop or repoint inbound FK constraints that shouldn't exist. If an inbound FK represents real data you can't yet sever, that table isn't dead — reconsider.

🚨 Outbound (this table pointing at LIVE tables) — the step that gets forgotten, and it breaks live pages. SET SCHEMA carries every outbound FK along, so the retired table keeps advertising itself in pg_constraint as a child of live tables. Any catalog-walking function then discovers it and queries it as the signed-in user, who has no USAGE on deprecated → 42501 permission denied for schema deprecated, and the whole call dies. That is exactly how every /projects/<id> page broke on 2026-08-13: get_project_references walked the FKs into projects.projects and hit two retired flashcard tables. Always finish the move with the idempotent sweep — it is safe and expected to re-run:

do $$ declare r record; begin
  for r in select conname, conrelid::regclass as from_table from pg_constraint
    where contype='f' and conrelid::regclass::text like 'deprecated.%'
      and confrelid::regclass::text not like 'deprecated.%'
  loop execute format('alter table %s drop constraint %I', r.from_table, r.conname); end loop;
end $$;

migrations/drop_graveyard_to_live_fks.sql is this sweep. It was run ONCE (2026-07-28) and treated as done; every table retired afterwards re-created the problem — 78 such FKs had accumulated by 2026-08-13. It is not a one-time migration, it is a post-condition of every deprecated move.

Step 4 — Deactivate the registration FIRST (enforced, 2026-08-12)

platform._enforce_entity_is_table now ERRORs when an ACTIVE entity_types row ends up pointing at deprecated — the DDL-sync trigger repoints schema_name during your SET SCHEMA, so moving a still-active registered table into deprecated fails with a check_violation. Deactivate before you move (this is the guard doing its job, not a bug):

update platform.entity_types set is_active=false where token='<token>';   -- or delete the row

Step 5 — Move it (idempotent, verify no data lost)

do $$ begin
  if to_regclass('public.<table>') is not null then
    execute 'alter table public.<table> set schema deprecated';
  end if;
end $$;
select count(*) from deprecated.<table>;   -- equals the pre-move count

Step 6 — De-register the rest

Remove the remaining platform footprint so nothing resolves to it:

delete from platform.entity_relationships where child_type='<token>' or parent_type='<token>';
delete from platform.shareable_resource_registry where table_name='<table>';

Leave satellite rows (associations/comments/…) keyed by the token in place unless they're now orphaned — sweep separately; they're harmless and reversible.

Step 7 — Cross-repo cleanup + finalize

deprecated IS in the db-types schema list, so the table still appears under the deprecated schema in FE types — that's fine; the point is to delete every code usage. Then run the finalize SOP (db-change/SKILL.md): pnpm db-types → remove FE usages → pnpm sync-types (fix TS); python db/generate.py → remove aidream usages + package_integration.py entry → python db/detect_applied.py → python run.py clean boot. Record the migration in the ledger. Commit + push main on both repos.

Clean cut — no silent shim (SKILL db-change → THE CUT)

SET SCHEMA deprecated makes public.<t> vanish → stale refs error (correct). Register it in scripts/dead-relations.json + platform.deprecated_relations and clear pnpm check:dead-relations (it finds the raw-SQL/Python/comment refs tsc misses). Never leave a readable old table or a compat view as a "fallback"; if it can't be moved yet, tripwire it (platform.deprecate_relation — data preserved, reads/writes RAISE).

NEVER

  • DROP TABLE — SET SCHEMA deprecated only (DROP is a separate PITR-gated step).
  • Leave the old name readable as a silent fallback — move it (vanishes → errors) or tripwire it (RAISES). Never a passthrough view.
  • Deprecate a table that still has live reads you haven't repointed or tracked.
  • Leave a generated type / Python model / package_integration.py reference pointing at the moved table.