wiki-vault/Sources/Homelab/2026-05-15-postgres-consolidation-phase2a-result.md
Travis Herbranson 52e3e76250 adding 2 files
2026-05-16 14:32:30 -04:00

12 KiB

created path project status tags type updated worker phase
2026-05-15 Sources/Homelab postgres-consolidation-reflection-layer active
ob1
mcp
pgvector
embeddings
session-note
session-note 2026-05-15 2 2a

Postgres Consolidation — Worker 2, Phase 2a result

Light Phase 2a complete. Disruption-free: no consumer connections broken, no live writers paused, no schema rename. openbrain is still the live DB name; petalbrain rename is Worker 3's cutover.

What landed

Inside openbrain (the existing DB):

New roles (no grants on ob1 yet — that schema doesn't exist)

Role Schema-level grants search_path
vault_mcp OWNER of wiki wiki, public
trellis_mcp OWNER of trellis trellis, public
ob1_mcp USAGE + SELECT on wiki and trellis ob1, public (ob1 not yet created)
lovebug USAGE + SELECT + INSERT/UPDATE/DELETE on wiki and trellis wiki, ob1, trellis, public

All four: LOGIN, NOSUPERUSER, NOCREATEDB, NOCREATEROLE, NOREPLICATION, NOBYPASSRLS, INHERIT.

Default privileges configured so future tables owned by vault_mcp (in wiki) and trellis_mcp (in trellis) inherit the same grant model automatically.

Credentials at /opt/backups/postgres-consolidation/credentials.env (mode 0600, herbyadmin-only). 64-char hex passwords. For KeePassXC transcription.

Schemas in openbrain

  • wiki — restored from Phase 1 herbylab-pg_dump, then ALTER SCHEMA herbylab RENAME TO wiki. Contains:

    • tables alembic_version (1 row), artifacts (0 rows), projects (0 rows)
    • sequences artifacts_id_seq, projects_id_seq (linked to IDENTITY columns)
    • enums artifact_type, project_status, project_domain
    • functions artifacts_set_updated_at, notify_project_change
    • triggers artifacts_updated_at_bump, projects_notify
    • FK constraint projects.artifact_idartifacts.id
    • all OID references updated transparently across the rename
  • trellis — restored from Phase 1 trellis-pg_dump (same name in source and target, no rename). Contains:

    • tables alembic_version (1), events (50), tags (42), thread_tags (55), threads (13)
    • sequences events_id_seq, tags_id_seq, threads_id_seq (OWNED BY linked)
    • FK chain across events ⇒ threads, thread_tags ⇒ threads, thread_tags ⇒ tags, threads.parent_id ⇒ threads
  • publicuntouched. public.thoughts still owned by postgres, still receiving writes from ob1-watcher (75 rows at end of Phase 2a, vs 74 at Phase 1 dump start, vs 70 at Phase 0 recon — ob1-watcher is healthy). Worker 3 moves this to ob1.

  • public.embeddingsnew, empty. Shared cross-schema embedding store per locked spec:

    public.embeddings (
      id              BIGSERIAL PRIMARY KEY,
      source_schema   TEXT NOT NULL,
      source_table    TEXT NOT NULL,
      source_id       BIGINT NOT NULL,
      embedding       VECTOR(768) NOT NULL,
      embedding_model TEXT NOT NULL,
      embedded_at     TIMESTAMPTZ NOT NULL DEFAULT now(),
      UNIQUE (source_schema, source_table, source_id, embedding_model)
    )
    

    Indexes:

    • embeddings_pkey (id)
    • embeddings_source_uniq UNIQUE (source_schema, source_table, source_id, embedding_model)
    • idx_embeddings_hnsw HNSW (embedding vector_cosine_ops) WITH (m=16, ef_construction=64) — matches the existing idx_thoughts_embedding_hnsw convention exactly
    • idx_embeddings_source (source_schema, source_table, source_id) — for orphan cleanup + source-aware queries
    • idx_embeddings_model (embedding_model) — for the multi-model coexistence case the plan calls out

    Owned by postgres (neutral shared infrastructure). All four service users have SELECT+INSERT+UPDATE+DELETE on the table and SELECT+USAGE on the embeddings_id_seq sequence. No FK — orphan cleanup is a separate periodic job (out of scope here per plan).

What Phase 2a did NOT do

  • Did not rename openbrainpetalbrain. That's a destructive cutover (live consumers must disconnect for ALTER DATABASE RENAME). Deferred to Worker 3.
  • Did not create the ob1 schema. Worker 3 owns the ALTER TABLE public.thoughts SET SCHEMA ob1 step + accompanying shape rewrite.
  • Did not touch public.thoughts in any way. Still in public, still writable, still being written to.
  • Did not pause ob1-watcher or ob1-enricher. Both still live.
  • Did not modify any MCP container connection strings or env files. All MCPs still connect to openbrain as before.
  • Did not touch the source herbylab or trellis databases. Both still exist on the instance with their original content; cleanup is Phase 8 of the plan.

Verification done

Row counts in openbrain match dump-source counts exactly

Schema/Table Live count Dump-source count
wiki.projects 0 0 (herbylab)
wiki.artifacts 0 0 (herbylab)
wiki.alembic_version 1 1 (herbylab)
trellis.threads 13 13
trellis.events 50 50
trellis.tags 42 42
trellis.thread_tags 55 55
trellis.alembic_version 1 1
public.thoughts 75 (live; was 74 at dump time)
public.embeddings 0 (new)

Source herbylab DB and source trellis DB confirmed unchanged.

Permission boundary smoke tests (positive isolation per plan Phase 3)

All ran via direct postgres connection on postgresql://<role>:<pw>@localhost:5432/openbrain:

Test Expected Result
vault_mcp SELECT wiki.artifacts succeed OK
vault_mcp INSERT trellis.threads DENY ERROR: permission denied for schema trellis
trellis_mcp SELECT wiki.artifacts DENY ERROR: permission denied for schema wiki
ob1_mcp SELECT wiki.artifacts + trellis.threads succeed OK (counts 0 + 13)
ob1_mcp INSERT wiki.artifacts DENY ERROR: permission denied for table artifacts
vault_mcp INSERT public.embeddings succeed OK (id 1)
ob1_mcp INSERT public.embeddings succeed OK (id 2)
trellis_mcp INSERT public.embeddings succeed OK (id 3)
lovebug INSERT public.embeddings succeed OK (id 4)

Smoke-test rows (4) deleted after verification; public.embeddings back to 0 rows.

Surprises / notes

  • Sequence ownership followed table. First pass on ALTER OWNER failed with cannot change owner of sequence "artifacts_id_seq" — Sequence "artifacts_id_seq" is linked to table "artifacts". The herbylab dump uses GENERATED ALWAYS AS IDENTITY, whose sequence is owned by the table and cannot be reassigned independently. Fixed by excluding relkind='S' from the table loop; sequence ownership now follows the table automatically. Trellis sequences use SERIAL/OWNED BY which has the same behavior. Caught + handled in one retry; final state correct.
  • ob1_mcp.search_path = ob1, public is set now even though the ob1 schema doesn't exist yet. Once Worker 3 creates the schema in the same DB (whatever its final name), unqualified table refs from ob1-mcp's connection will resolve there automatically.
  • No vault-mcp env update was needed for Phase 2a. The current HERBYLAB_DATABASE_URL in vault-mcp points at hostname vault-postgres (still anomalous — part of Travis's rename rollout) and DB herbylab. Neither is wired into the consolidated state yet. Phase 3 of the original plan handles MCP connection cutover.

Worker 3 readiness assessment

Worker 3's cutover window is the next destructive step. From the priming, Worker 3 must:

  1. Pause ob1-watcher + ob1-enricher + ob1-mcp.
  2. Take Phase 2-start fresh pg_dump of openbrain, herbylab, trellis to /opt/backups/postgres-consolidation/ with suffix -phase2-start-<UTC-timestamp>. Verify via test restore.
  3. ALTER DATABASE openbrain RENAME TO petalbrain (now safe — no live connections).
  4. Connect to petalbrain and ALTER TABLE public.thoughts SET SCHEMA ob1, plus all dependent objects (indexes are intra-table so they follow; the match_thoughts function references thoughts directly without schema qualification so search_path must include ob1 when ob1_mcp calls it — already set; the thoughts_updated_at trigger follows the table; update_updated_at() function lives in public and should be moved or be referenced cross-schema).
  5. Create ob1 schema if SET SCHEMA didn't auto-create it (SET SCHEMA does create the schema if it didn't exist — but actually it requires the schema to pre-exist; Worker 3 should CREATE SCHEMA ob1 AUTHORIZATION ob1_mcp first).
  6. ob1-specific grants: ob1_mcp owns ob1.*; lovebug gets full RW; vault_mcp and trellis_mcp get nothing.
  7. Backfill embeddings: for each of the 75-ish rows in ob1.thoughts, insert into public.embeddings with source_schema='ob1', source_table='thoughts', source_id=<row id>, embedding=<existing embedding column value>, embedding_model='nomic-embed-text'.
  8. Decide: keep the ob1.thoughts.embedding column post-backfill (redundant with public.embeddings) or drop it. Plan says all embeddings live in public.embeddings; suggests dropping. But that changes the row shape for ob1-mcp's code. Worker 3 call.
  9. Rewrite ob1-mcp's add_thought and search_thoughts for the new shape (target table ob1.thoughts, embedding pushed to public.embeddings separately, search joins).
  10. Update connection strings on ob1-mcp, ob1-watcher, ob1-enricher from DB openbrain to DB petalbrain. Watcher talks to ob1-mcp HTTP only, so its env may not need DB host change — verify.
  11. Update trellis-mcp and herbydev-mcp / vault-mcp connection strings from their current DBs (trellis, herbylab) to the consolidated petalbrain with their service-user credentials from credentials.env.
  12. Resume ob1-watcher + ob1-enricher.
  13. Verify: row counts, semantic search query roundtrip, NOTIFY trigger on wiki.projects still emits.

Phase 2a outputs Worker 3 will need:

  • Service user passwords in /opt/backups/postgres-consolidation/credentials.env.
  • Phase 1 backup set in /opt/backups/postgres-consolidation/ plus the rollback runbook there.
  • This session note.

Holding pattern

ob1-watcher and ob1-enricher continue writing to public.thoughts. Each minute that passes, the gap between the Phase 1 dump's thoughts snapshot (74) and the cutover-time count grows. Phase 2-start fresh dump shrinks that gap to seconds when Worker 3 runs. Until then: no degradation, no risk; just normal capture.

Worker 2 standing by. No further actions until Worker 3 spawns or Travis redirects.