wiki-vault/Sources/Homelab/2026-05-15-postgres-consolidation-phase2a-result.md
Travis Herbranson 30e2483e71 lint: fix 4 session-note types, 6 broken wikilinks
- type: session-note -> session-notes (4 files: 2 vault-integration-test, 2 postgres-consolidation phases)
- [[Sources/Homelab/ob1-jsonl-watcher]] -> actual filename (5 links across ob1, ai-memory synthesis, Homelab index)
- [[Sources/Dev/env-file-hardening]] -> Sources/Homelab/env-file-hardening (gitea entity)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 21:30:15 -04:00

253 lines
12 KiB
Markdown

---
created: '2026-05-15'
path: Sources/Homelab
project: postgres-consolidation-reflection-layer
status: active
tags:
- ob1
- mcp
- pgvector
- embeddings
- session-note
type: session-notes
updated: '2026-05-15'
worker: 2
phase: 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_id``artifacts.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`
- `public`**untouched**. `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.embeddings`**new, 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 `openbrain``petalbrain`. 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.