second-brain/postgres-migration-planning.md
Travis Herbranson 6c1445c8ed docs: update CLAUDE.md + migration plan to reflect shipped Postgres design
CLAUDE.md updates:
- new src/second_brain/embeddings/ entry in the project layout
- setup section now lists the postgres-superuser bootstrap (CREATE SCHEMA
  AUTHORIZATION lovebug) and the alembic step
- env var table now covers SECOND_BRAIN_DATABASE_URL, OLLAMA_URL,
  EMBEDDING_MODEL, and the pool sizing knobs
- conventions section gained five new gotchas (the lovebug CREATE gap,
  public.embeddings ownership, best-effort embedding, etc.)

postgres-migration-planning.md flipped from "planning questions" to a
"what shipped" runbook — locked decisions table, operator setup steps,
known gotchas, and the next-iteration backlog.
2026-05-24 22:57:31 -04:00

119 lines
5.3 KiB
Markdown

# Postgres + pgvector migration — implementation notes
Status: **shipped** (2026-05-24). Originally a planning checklist; rewritten
to capture the implemented design and the decisions that landed. Kept in the
repo so the next iteration has the "why" without grepping commits.
---
## What shipped
- Backing store switched from SQLite to the existing **petalbrain** Postgres
instance (pgvector/pg17), connecting over the homelab docker network at
`homelab-postgres:5432`.
- Relational tables (`sources`, `extractions`, `wiki_pages`) live in a new
`second_brain` schema owned by the **`lovebug`** role — no dedicated service
role.
- Embeddings reuse the **shared `public.embeddings` table** (HNSW vector index
already in place). Rows are keyed by
`(source_schema='second_brain', source_table='extractions', source_id, embedding_model='nomic-embed-text')`.
For this round we embed **extraction summaries only** (not transcript
chunks, not key_points / claims).
- Embedding pipeline mirrors `vault_mcp.core.embeddings`: shared
`embedding_chunking` for 512/64-token chunking → Ollama
`nomic-embed-text` (768-d, `num_ctx=8192`) → delete-before-insert upsert
with vector text literals (`%s::vector`).
- **Graceful degradation** is the rule: missing DB URL, Ollama down, or any
DB error logs a warning and returns `None`. The relational row and the
file-based vault stay canonical.
- **Alembic** stands up the `second_brain` schema; `public.embeddings` is
managed out-of-band and intentionally outside the migration tree.
- File-based wiki compiler is unchanged. No dual-write to `petalbrain.wiki`.
- **No SQLite import** — we started clean on Postgres.
---
## Locked decisions (from the planning round)
| Question | Decision |
|---|---|
| DB instance | Existing petalbrain (homelab-postgres) |
| Schema | New `second_brain` schema (owned by `lovebug`) |
| Connection | Containerised: `homelab-postgres:5432` (NOT host `127.0.0.1:5433`) |
| Pooling | `psycopg_pool.ConnectionPool` min=1/max=10 — no PgBouncer in front |
| Driver | psycopg v3, SQLAlchemy uses `postgresql+psycopg://` |
| PK style | Int autoincrement (`SERIAL`) — kept |
| Timestamps | Naive UTC `timestamp without time zone` — kept |
| Embedding scope | Extraction summaries only (Phase 1) |
| Embedding model | `nomic-embed-text` (768-d), Ollama via `OLLAMA_URL` |
| Migration tool | Alembic, second-brain owns its own tree |
| `public.embeddings` | NOT managed by our Alembic — shared with vault-mcp / ob1 |
| File-based wiki | Kept intact; no dual-write into petalbrain.wiki |
| Old SQLite data | Not imported — clean start |
---
## Setup (operator runbook)
```bash
# 1. One-time, as postgres superuser, bootstrap the schema. lovebug doesn't
# have CREATE on petalbrain, so this can't be done from the app side.
docker exec homelab-postgres psql -U postgres -d petalbrain -c \
"CREATE SCHEMA second_brain AUTHORIZATION lovebug;
GRANT ALL PRIVILEGES ON SCHEMA second_brain TO lovebug;"
# 2. Point the project at the DB (env wins over settings.toml).
export SECOND_BRAIN_DATABASE_URL=\
"postgresql+psycopg://lovebug:<pw>@homelab-postgres:5432/petalbrain"
# 3. Install deps and run the migration.
uv sync
uv run alembic upgrade head
```
The `lovebug` password lives in `/opt/backups/postgres-consolidation/credentials.env`
(0600, host-only).
---
## Key files (post-migration)
- `src/second_brain/models.py` — schema-qualified ORM, `__table_args__`
`second_brain`; native Postgres enum types are scoped to the schema too.
- `src/second_brain/database.py` — psycopg-v3 engine + URL resolution
(constructor arg → `SECOND_BRAIN_DATABASE_URL``HERBYLAB_DATABASE_URL`
`[database].url`).
- `src/second_brain/embeddings/__init__.py` — near-verbatim port of
`vault_mcp/core/embeddings.py` adapted to our key tuple.
- `alembic.ini` + `alembic/env.py` — schema-pinned Alembic; the env.py guards
`CREATE SCHEMA` behind `pg_namespace` because `lovebug` lacks `CREATE` on
the database.
- `alembic/versions/4a1e2f6c9d10_v1_second_brain_pipeline_tables.py` — v1
migration: enum types + `sources` + `extractions` + `wiki_pages`.
- `tests/test_smoke_embedding.py` — end-to-end smoke test exercising the full
Postgres + Ollama path. Auto-skips when either is unreachable.
---
## Known gotchas
1. **`CREATE SCHEMA IF NOT EXISTS` errors as `lovebug`** even when the schema
exists. `pg_namespace` lookup in `alembic/env.py` is the workaround.
2. **`+psycopg` URL prefix.** SQLAlchemy needs it; raw psycopg doesn't.
`database.py` adds it if missing; the embeddings module strips it.
3. **Pool conflict.** `database.py` opens an SQLAlchemy pool; `embeddings/__init__.py`
opens an independent psycopg pool. Both share the same DSN; both are small
(min=1) so they don't strain the cluster.
4. **`anthropic-skills:zero-check` needs `pytest-cov`.** Added to the new
`[dependency-groups].dev` block so `uv sync` resolves it.
---
## Next iterations (open, not blocked)
- Embed transcript chunks (or at least key_points / claims), not just summaries.
- Promote vector retrieval into the web UI (semantic search across extractions).
- Add a periodic backfill script that re-embeds extractions whose
`embedding_model` no longer matches the current default.
- Decide on the production vault path (`/opt/projects/second-brain-vault/`?).