FED-M1-05 — Adds mosaic storage migrate-tier --to federated for one-way migration from local (PGlite) or standalone (Postgres without pgvector) → federated (Postgres + pgvector).
Architectural correction during review
The first implementation read source data via PgliteAdapter (flat (id, data jsonb) collection schema). Independent code review surfaced this as a P0 bug: the gateway writes domain data via the normalized Drizzle schema (createPgliteDb() → DB injection), not via the storage adapter. Reading from the flat collections would have silently produced empty federated databases.
Fix: introduced DrizzleMigrationSource that queries the same normalized schema the gateway writes through. Both local and standalone sources now route through Drizzle (createPgliteDb / createDb from @mosaicstack/db), guaranteeing the migration captures real domain data.
Behaviors
--dry-run — prints per-table row counts; no writes
--allow-non-empty — required to write into a populated target (default refuses)
Per-table transactions — partial failure does not leave half-migrated tables
Idempotent — INSERT ... ON CONFLICT (id) DO UPDATE keyed on primary key
Topological FK ordering verified against schema.references()
## Summary
FED-M1-05 — Adds `mosaic storage migrate-tier --to federated` for one-way migration from `local` (PGlite) or `standalone` (Postgres without pgvector) → `federated` (Postgres + pgvector).
## Architectural correction during review
The first implementation read source data via `PgliteAdapter` (flat `(id, data jsonb)` collection schema). Independent code review surfaced this as a P0 bug: the gateway writes domain data via the **normalized Drizzle schema** (`createPgliteDb()` → `DB` injection), not via the storage adapter. Reading from the flat collections would have silently produced empty federated databases.
Fix: introduced `DrizzleMigrationSource` that queries the same normalized schema the gateway writes through. Both `local` and `standalone` sources now route through Drizzle (`createPgliteDb` / `createDb` from `@mosaicstack/db`), guaranteeing the migration captures real domain data.
## Behaviors
- `--dry-run` — prints per-table row counts; no writes
- `--allow-non-empty` — required to write into a populated target (default refuses)
- Per-table transactions — partial failure does not leave half-migrated tables
- Idempotent — `INSERT ... ON CONFLICT (id) DO UPDATE` keyed on primary key
- Topological FK ordering verified against `schema.references()`
- Skipped tables: `sessions`, `verifications`, `admin_tokens` (TTL'd / one-time / env-bound)
- `insights.embedding` projected to NULL when source lacks pgvector (column is nullable per schema)
## Tests
32 unit tests in `packages/storage/src/migrate-tier.spec.ts`:
- Topological order correctness
- Dry-run path takes no writes
- Idempotency (re-run produces same target state)
- Empty-target precondition gating `--allow-non-empty`
- Skipped tables not in migration order
- Embedding-null projection for non-pgvector sources
```
Test Files 1 passed (1)
Tests 32 passed (32)
```
## Test plan
- [ ] CI green (typecheck + lint + format + test gates)
- [ ] Real PG ↔ PGlite migration verified by FED-M1-08 (next task)
- [ ] Manual smoke test once doctor (FED-M1-06) and integration test (FED-M1-07) land
Refs #460
Adds `mosaic storage migrate-tier --to federated` for one-way migration
from `local` (PGlite) or `standalone` (PG without pgvector) to
`federated` (PG + pgvector). Source is read via DrizzleMigrationSource
which queries the normalized schema (the same path the gateway writes
through), so the migration captures all real domain data — not just
the flat key/value collections.
Key behaviors:
- `--dry-run` prints per-table row counts without writes
- `--allow-non-empty` required to write into a populated target (default
refuses to mix data sets)
- Per-table transactions; partial failure does not leave half-migrated
tables
- Idempotent on re-run via INSERT ... ON CONFLICT DO UPDATE keyed on id
- Topological table order respects FK dependencies (verified against
schema.references())
- Skips sessions, verifications, admin_tokens (TTL'd / one-time / env-bound)
- For sources without pgvector, insights.embedding is projected to NULL
on read, then upserted as NULL (column is nullable)
32 unit tests cover ordering, dry-run, idempotency, empty-target
preconditions, table skipping, and embedding-null projection. Integration
test against real PG/PGlite is FED-M1-08.
Refs #460
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
FED-M1-05 — Adds
mosaic storage migrate-tier --to federatedfor one-way migration fromlocal(PGlite) orstandalone(Postgres without pgvector) →federated(Postgres + pgvector).Architectural correction during review
The first implementation read source data via
PgliteAdapter(flat(id, data jsonb)collection schema). Independent code review surfaced this as a P0 bug: the gateway writes domain data via the normalized Drizzle schema (createPgliteDb()→DBinjection), not via the storage adapter. Reading from the flat collections would have silently produced empty federated databases.Fix: introduced
DrizzleMigrationSourcethat queries the same normalized schema the gateway writes through. Bothlocalandstandalonesources now route through Drizzle (createPgliteDb/createDbfrom@mosaicstack/db), guaranteeing the migration captures real domain data.Behaviors
--dry-run— prints per-table row counts; no writes--allow-non-empty— required to write into a populated target (default refuses)INSERT ... ON CONFLICT (id) DO UPDATEkeyed on primary keyschema.references()sessions,verifications,admin_tokens(TTL'd / one-time / env-bound)insights.embeddingprojected to NULL when source lacks pgvector (column is nullable per schema)Tests
32 unit tests in
packages/storage/src/migrate-tier.spec.ts:--allow-non-emptyTest plan
Refs #460