Skip to content

fix(observation): migrate legacy person_observations.title -> titles[] on startup#199

Closed
sroussey wants to merge 1 commit into
harnessfrom
claude/wonderful-hypatia-t5btwy
Closed

fix(observation): migrate legacy person_observations.title -> titles[] on startup#199
sroussey wants to merge 1 commit into
harnessfrom
claude/wonderful-hypatia-t5btwy

Conversation

@sroussey

Copy link
Copy Markdown
Contributor

Summary

The recent rename of person_observations.title (TEXT) to titles (JSON-encoded array on SQLite, JSONB on Postgres) happened in place with no ALTER TABLE and no version-registry migration. Any operator with a pre-existing database would:

  • Hit column titles does not exist on the next observePerson write, or
  • Keep writing while the older title data was orphaned.

This PR adds a one-shot idempotent startup migration invoked immediately before PERSON_OBSERVATION_REPOSITORY_TOKEN.setupDatabase(), mirroring the shape and dispatch of migrateLegacyForm8KEventsTable.

Files

  • NEW src/storage/observation/PersonObservationTitleMigration.ts — dual-backend one-shot migration.
    • SQLite: probes sqlite_master / PRAGMA table_info; if titles is absent adds titles TEXT; if title is present backfills titles = json_array(title) on rows whose titles is still null, then drops title. Wrapped in a single BEGIN/COMMIT, with ROLLBACK on throw.
    • Postgres: probes information_schema; runs ADD COLUMN IF NOT EXISTS "titles" JSONB, backfills via jsonb_build_array("title") under the same null-target guard, then DROP COLUMN IF EXISTS "title". All in one transaction. JSONB matches what the ORM actually emits — the array-element whitelist in mapPostgresType explicitly excludes VARCHAR, so Type.Array(Type.String({maxLength:256})) falls through to JSONB rather than a native VARCHAR(256)[].
    • Dry-run mode short-circuits before touching the database.
    • Silent on a fresh DB; emits one info line per backend when a real migration ran (column added, or rows backfilled).
  • EDITED src/config/setupAllDatabases.ts — import the migration and call it directly before PERSON_OBSERVATION_REPOSITORY_TOKEN.setupDatabase().
  • NEW src/storage/observation/PersonObservationTitleMigration.test.ts — SQLite vitest coverage:
    1. Legacy shape with title column and three seeded rows ('CEO', 'Chief Executive Officer and Director', NULL) — after migration, titles exists, title gone, values are ["CEO"], ["Chief Executive Officer and Director"], null.
    2. Already-migrated shape — no-op, single row's titles value preserved.
    3. Absent table — no throw, table stays absent.
    4. Half-migrated (both columns) — row with title='CEO', titles=NULL becomes ["CEO"]; row with title='Old', titles='["New"]' keeps ["New"]; title column dropped.

Test plan

  • bun test src/storage/observation/PersonObservationTitleMigration.test.ts — 4/4 passing.
  • bun test src/storage/observation/ — 24/24 passing across 5 files.
  • bun test src/storage/form-8k-event/Form8KEventLegacyMigration.test.ts — 3/3 passing (regression on shared reset-DI dance).
  • bun run build — clean build (bundle + tsc).
  • Postgres branch — deferred to manual verification (mirrors the Form-8K migration precedent).

Notes


Generated by Claude Code

…] on startup

The `person_observations.title` (TEXT) column was renamed in place to
`titles` (JSON-encoded array on SQLite; JSONB on Postgres) with no
ALTER TABLE and no version-registry migration. Operators upgrading from
before that change would hit "column titles does not exist" on the next
`observePerson` write, or keep writing while the older `title` data was
orphaned.

Adds a one-shot idempotent startup migration that runs right before
`PERSON_OBSERVATION_REPOSITORY_TOKEN.setupDatabase()`:

- SQLite: adds `titles TEXT` if missing, backfills each non-null `title`
  as `json_array(title)` into rows whose `titles` is still null, then
  drops `title`.
- Postgres: adds `titles JSONB` if missing, backfills via
  `jsonb_build_array("title")` under the same null-target guard, then
  drops `title`. (JSONB matches what the ORM emits — the array-element
  whitelist in `mapPostgresType` excludes VARCHAR, so an array-of-
  VARCHAR schema falls through to JSONB rather than a native array.)

Both branches wrap the writes in a single transaction and probe the
schema first, so the migration is a no-op on a fresh DB and on an
already-migrated DB. Dry-run mode short-circuits before touching the
database.

Verification: `bun test src/storage/observation/`,
`bun test src/storage/form-8k-event/Form8KEventLegacyMigration.test.ts`,
and `bun run build` all green. Postgres coverage is deferred to manual
verification (mirrors the Form-8K migration precedent).

Operator note: silent on a fresh database; emits one info line per
backend when a real migration runs (column added, or rows backfilled).
@sroussey sroussey closed this Jul 16, 2026
@sroussey
sroussey deleted the claude/wonderful-hypatia-t5btwy branch July 17, 2026 00:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants