Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
5.8 KiB
Legacy MySQL → PostgreSQL schema map
Descrybe v2 replaces Clerk-backed MySQL identity with first-party auth on PostgreSQL. The migrator (packages/migrator / apps/api/cmd/migrator) reads legacy rows, remaps IDs, and loads into the goose schema under apps/api/sql/schema.
Identity remapping
| Legacy (MySQL) | v2 (PostgreSQL) | Notes |
|---|---|---|
Clerk user_id (text, e.g. user_…) |
users.legacy_user_id + new users.id UUID |
Every FK that pointed at Clerk user ids is rewritten to the new UUID via the ID map |
companies.id (text) |
companies.legacy_company_id + new companies.id UUID |
Preserve the old text id in legacy_company_id for audit / re-runs; all new FKs use the UUID PK |
profiles (user ↔ company + role/status) |
memberships |
One row per (company_id, user_id); roles admin | member; status active | inactive |
admin_users |
users.is_platform_admin |
Platform-wide flag on the user row (not a separate table) |
| Clerk org membership | memberships |
No Clerk org ids in v2 |
| Clerk sessions / passwords | sessions + users.password_hash |
No Clerk hashes to import; set must_set_password = true and email set-password links |
Users
- For each distinct Clerk
user_id(fromusers/profiles/ related tables), allocate a new UUID. - Store the Clerk id in
users.legacy_user_id(unique). - Copy email / name; leave
password_hashnull; setmust_set_password = true. - Rewrite all user FKs through the user ID map.
Companies
- For each legacy company row, allocate a new UUID PK.
- Store the legacy text id in
companies.legacy_company_id(unique). - Copy name, language, merge-by-GTIN and related settings into
companies/company_settings. - Rewrite all
company_idtext FKs through the company ID map.
Profiles → memberships
Legacy profiles rows become memberships:
- Map
profiles.user_id→memberships.user_id(new UUID) - Map
profiles.company_id→memberships.company_id(new UUID) - Map role/status enums to the v2 check constraints
- Skip duplicate
(company_id, user_id)after remapping; prefer the active/admin row if conflict
Admin users
Legacy admin_users emails/ids set users.is_platform_admin = true on the matching remapped user. Do not create orphan admin rows without a user.
Products index cleanup (raw_products / processed_products)
Legacy Drizzle schemas declare overlapping single-column indexes (same column indexed twice under different names). Do not recreate those duplicates in Postgres.
Legacy raw_products — drop / consolidate
| Legacy index | Columns | v2 action |
|---|---|---|
idx_raw_products_feed_id |
feed_id |
Keep one feed index (or rely on composite) |
idx_raw_products_feed |
feed_id |
Drop — duplicate of above |
idx_raw_products_company_id |
company_id |
Prefer composite (company_id, …) instead of bare duplicate |
idx_raw_products_company |
company_id |
Drop — duplicate |
idx_raw_products_processed |
is_processed |
Re-evaluate; often redundant with processing_status |
idx_raw_products_processing_status |
processing_status |
Keep if status filters are hot |
idx_raw_products_company_feed |
(company_id, feed_id) |
Keep — primary list/filter path |
idx_raw_products_gtin |
gtin |
Keep; consider (company_id, gtin) if lookups are tenant-scoped |
idx_raw_products_sync_job |
sync_job_id |
Keep if sync cleanup queries need it |
idx_raw_products_file |
file_id |
Keep if upload paths filter by file |
raw_products_gtin_feed_unique |
(gtin, feed_id) |
Preserve uniqueness (or company-scoped unique after design) |
Post-load: create remaining indexes with CREATE INDEX CONCURRENTLY, then ANALYZE.
Legacy processed_products — redesign notes
| Legacy index | Columns | v2 action |
|---|---|---|
idx_products_company |
company_id |
Prefer (company_id, status) or (company_id, feed_id) composites from real list queries |
idx_products_feed |
feed_id |
Keep if feed-scoped exports need it; else fold into composite |
idx_processed_products_raw |
raw_product_id |
Keep — join back to raw |
idx_products_product_id |
product_id |
Keep only if external product ids are looked up; else drop |
Also: remapped user_id / company_id text → UUID FKs; drop loose profile-based company links; unique constraints only where the product model requires them (e.g. company + gtin when merge-by-GTIN is on). High-volume tables may use BIGSERIAL PKs instead of UUIDs.
ID map JSON format
Migrator writes (and accepts) a JSON document used for dry-run reports, resume, and FK rewrite.
{
"version": 1,
"generated_at": "2026-08-03T18:00:00Z",
"source": "mysql",
"target": "postgres",
"users": {
"user_2abcClerkId": "550e8400-e29b-41d4-a716-446655440000"
},
"companies": {
"org_or_legacy_company_text_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8"
},
"meta": {
"user_count": 1,
"company_count": 1,
"dry_run": false
}
}
Rules:
- Keys are legacy string ids; values are new UUID strings.
- File is the single source of truth for a migration run; re-runs must reuse the same map to stay idempotent.
- Optional per-entity sections (e.g.
categories,feeds) may be added later with the same{ legacy_id: new_uuid }shape. - Store next to migration artifacts (e.g.
artifacts/id-map.json); do not commit production maps with PII beyond opaque ids.
Load order (FK-safe)
- companies
- users
- memberships / invites / api_keys / company_settings
- plans → company_plans → billing_cycles → credit_balances → processing_costs
- catalog (categories, attributes, variables, files, products)
- feeds / export / jobs (skip ephemeral queues)
See cutover.md for the operational runbook.