# Migrate `descrybe_new.sql` → v2 Postgres (A1 only) **Date:** 2026-08-08 (local reseed from `descrybe_new (1).sql`) **Status:** **PASS** **Scope:** Single tenant **A1 Slovenija** only (all other dump tenants skipped / cleaned) Secrets are masked below. Do not commit `.env`, MySQL passwords, `artifacts/`, or invite tokens. ## What “a1” resolved to | Field | Value | |-------|--------| | MySQL company name | `A1 Slovenija` | | MySQL / legacy `company_id` | `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` | | Match used | `companies.name` LIKE `%A1%` → exact row above (only A1 hit) | | Postgres after migrate + demo | **A1 Slovenija** (dump name kept) | | Postgres `companies.id` | `604f23a8-b66e-4b21-8b45-0d72b68f4790` (stable via `artifacts/descrybe-new-a1/id-map.json`) | | `legacy_company_id` | `97e1a309-3d23-4aa2-b518-8e8d7afdfec7` | **Company naming:** Keep dump name **A1 Slovenija**. Do not rename to “Local Demo Co”. Wallet/plan come from MySQL `credit_balances` / plan **A1** (2500 total / 163 used / 2337 remaining on 2026-08-08 dump) — not a fake 1M demo pack. ## Inputs | Item | Value | |------|--------| | Dump | `C:\Users\Green Eclipse\Downloads\descrybe_new (1).sql` (~549 MB Adminer MySQL 8 dump) | | Temp MySQL DB | Laragon `descrybe_new` (left intact; did **not** overwrite `descrybe_v2`) | | Postgres | Docker `descrybe-v2-postgres` host port **5433** | | Postgres DSN (local default) | `postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable` | | Migrator | `apps/api/cmd/migrator` | | Maps / report | `artifacts/descrybe-new-a1/` (gitignored); copy `docs/migration-reports/migration-report-a1-latest.json` | ## Commands run ### 1. Wipe v2 Postgres only (keep Docker volume) ```powershell docker exec descrybe-v2-postgres psql -U descrybe -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'descrybe' AND pid <> pg_backend_pid();" docker exec descrybe-v2-postgres psql -U descrybe -d postgres -c "DROP DATABASE IF EXISTS descrybe;" docker exec descrybe-v2-postgres psql -U descrybe -d postgres -c "CREATE DATABASE descrybe OWNER descrybe;" ``` ### 2. Goose migrations (001–024) ```powershell cd f:\laragon\www\_MY\descrybe-v2\apps\api $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" go run github.com/pressly/goose/v3/cmd/goose@v3.24.3 -dir sql/schema postgres $env:DATABASE_URL up ``` Note: `024_ai_prompts.sql` had a UTF-8 BOM (`\ufeff`); stripped so goose could parse (same class of fix as earlier `006_feed_sync.sql`). ### 3. Import dump into temp MySQL (streamed; ~2 min) ```powershell # create empty DB, then stream file into mysql client (do not load 862MB into PowerShell) mysql -uroot -p**** -e "DROP DATABASE IF EXISTS descrybe_new; CREATE DATABASE descrybe_new CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # stdin stream of descrybe_new.sql → descrybe_new ``` Dump-wide MySQL counts (before filter): companies 31, categories 8465, attributes 198319, xml_feeds 33, raw_products 159070, processed_products 7953, export_feeds 11. ### 4. A1-only migrator ```powershell cd f:\laragon\www\_MY\descrybe-v2\apps\api $env:DATABASE_URL = "postgres://descrybe:descrybe@localhost:5433/descrybe?sslmode=disable" $env:MIGRATE_MYSQL_DSN = "root:****@tcp(localhost:3306)/descrybe_new?parseTime=true" $maps = "f:\laragon\www\_MY\descrybe-v2\artifacts\descrybe-new-a1" $a1 = "97e1a309-3d23-4aa2-b518-8e8d7afdfec7" go build -o $env:TEMP\descrybe-migrator.exe ./cmd/migrator # dry-run then live (filter at migrate time) & $env:TEMP\descrybe-migrator.exe -mysql $env:MIGRATE_MYSQL_DSN -postgres $env:DATABASE_URL ` -company $a1 -dry-run -maps-dir $maps -report-dir $maps -resume=false & $env:TEMP\descrybe-migrator.exe -mysql $env:MIGRATE_MYSQL_DSN -postgres $env:DATABASE_URL ` -company $a1 -maps-dir $maps -report-dir $maps -resume ` -ensure-demo -demo-email demo@descrybe.local -demo-password 'DemoPass123!' ` -skip-post-import ``` Live elapsed ≈ **32s**. Validation: `mode: live ok=true`, orphan checks **17/17 PASS**. ### 5. Seed demo (Enterprise + API key + `.test` alias) ```powershell go run ./cmd/seed-demo -postgres $env:DATABASE_URL ` -email demo@descrybe.local -password 'DemoPass123!' ``` ### 6. Post-cleanup (junk from global user import) Migrator still upserts **all** MySQL profile→user rows even with `-company` (memberships are filtered). Removed users with **no** membership (kept demo emails): ```sql DELETE FROM users u WHERE NOT EXISTS (SELECT 1 FROM memberships m WHERE m.user_id = u.id) AND lower(u.email) NOT IN ('demo@descrybe.local','demo@descrybe.test'); -- deleted 22 orphan users ``` ### 6b. Import A1 job history (optional follow-up) Job history was skipped in the initial cutover. Re-run with domain `jobs` (rows tagged `ai_provider_mode=migrated` so 30-day retention does not purge them): ```powershell & $env:TEMP\descrybe-migrator.exe -mysql $env:MIGRATE_MYSQL_DSN -postgres $env:DATABASE_URL ` -company $a1 -domains jobs -maps-dir $maps -report-dir $maps -resume -skip-post-import ``` Local/dev (**outdated — do not follow**): demo no longer owns A1; use **Platform Demo** + `a1-primary@` per [safe-test-fixtures.md](safe-test-fixtures.md). Historical logins: | Role | Email | Password | |------|-------|----------| | Demo admin | `demo@descrybe.local` (alias `.test`) | `DemoPass123!` | | Primary A1 | `a1-primary@descrybe.local` | `DemoPass123!` | Primary maps to Clerk `user_30AqqJ8uepxvPUzDSqy81U5w6Ll`. Other A1 MySQL profiles were removed after migrate (jobs reassigned to primary). To reset the primary password: ```powershell go run ./cmd/migrator -postgres $env:DATABASE_URL ` -set-password 'a1-primary@descrybe.local:DemoPass123!' ``` ### 6c. Local Demo Co user cleanup (2026-08-05) MySQL A1 (`97e1a309-…`) had **5** `profiles.user_id` values. Preference: keep **demo + primary only**. | Action | Detail | |--------|--------| | Kept | `demo@descrybe.local`, `demo@descrybe.test`, `a1-primary@descrybe.local` (`user_30AqqJ…`) | | Deleted (4) | `user_2tJxu…`, `user_2yaUP…`, `user_2zzh…`, `user_3005r…` (`@legacy.local`) | | Reassigned | 3 `processing_jobs.user_id` → primary (280 jobs on primary; 51 still null) | | Catalog | Unchanged — **4378** processed products, 331 jobs | **Do not restore** those four `@legacy.local` members. A later agent re-inserted them for switcher labeling; they were deleted again. Local Demo Co stays at **3 users** / **3 memberships**. UserSwitcher labels: **Demo admin** (`.local` + `.test`) and **A1 · …81U5w6Ll** (`is_primary_a1`). ## Row counts — kept vs skipped ### Migrator written (A1 filter) | Entity | Written / kept | Notes | |--------|---------------:|-------| | companies | 1 | Only A1; renamed → Local Demo Co | | memberships | 5 → **3** after cleanup | A1 primary + 2 demo admins | | users (after cleanup) | **3** | demo `.local` + demo `.test` + `a1-primary@descrybe.local` | | plans | seed + migrated catalog | Global plan rows OK for local billing | | company_plans | 1 → Enterprise via seed-demo | Other tenants’ plans skipped | | credit_balances | 1 | 1M Enterprise credits after seed | | categories | 119 | | | attributes | 303 | | | category_attributes | 307 | | | custom_variables | 3 | | | input_feeds (xml) | 12 | | | feed_mappings | 11 | ComTrade has 0 mappings in source too | | raw_products | 23748 | MySQL A1 had 56132; GTIN-deduped under `(company_id, gtin)` | | processed_products | 4330 | | | export_feeds | 2 | | | files | 0 | Dump had 1 file metadata row for another context / skipped | | processing_jobs | 179 | via `-domains jobs` (`ai_provider_mode=migrated`) | ### Skipped (other tenants / noise) | Item | Approx. skipped | |------|----------------:| | Other companies | 30 of 31 | | Non-A1 categories / attributes / feeds / products / exports | Rest of dump (see MySQL totals above) | | Orphan users after cleanup | 22 | | `company_plans` for non-A1 | 4 skipped in migrator log | | `credit_balances` for non-A1 | 7 skipped | | API keys / Clerk passwords / job history | Never imported | ### Final Postgres snapshot (A1 Slovenija — 2026-08-08) | Metric | Count | |--------|------:| | companies | 1 (+ `__platform_settings__`) | | users | **3** (demo.local, demo.test, a1-primary) | | memberships | **3** | | categories | 119 (108 with AI prompts overlay) | | attributes | 303 | | category_attributes | 307 | | input_feeds | 12 | | feed_mappings | 11 | | raw_products | 23748 | | processed_products | 4330 | | export_feeds | 2 | | processing_jobs | 179 | | custom_variables | 3 | | wallet | 2500 / 163 used / 2337 remaining | ## Demo admin (local only) | Field | Value | |-------|--------| | Email | `demo@descrybe.local` (alias `demo@descrybe.test`) | | Password | `DemoPass123!` (already documented in `docs/demo-user.md`) | | Role | Platform admin + company **admin** on Local Demo Co | | Plan | Enterprise — 1_000_000 AI credits, unlimited SKUs | | API key | `dk_demo_local_descrybe_test_key_v1` (hash only in DB) | Web: http://localhost:5174/login · API: http://localhost:8080 ## Verification (this run) | Check | Result | |-------|--------| | `GET /healthz` | ok | | `GET /readyz` | database ok | | `POST /api/auth/login` (CSRF cookie + Origin) | **200** → company Local Demo Co | | `GET /api/v1/products?limit=1` (demo API key) | **200**, `meta.total=4378` | | `GET /api/v1/feeds` | **200**, 12 feeds | ## Related docs - [portable-mysql-pg-migration.md](portable-mysql-pg-migration.md) — migrator flags / domains - [demo-user.md](demo-user.md) — demo credentials & Enterprise wallet - [migration-run-log.md](migration-run-log.md) — prior full-tenant rehearsal - Report: [migration-reports/migration-report-a1-latest.json](migration-reports/migration-report-a1-latest.json)