docs: sync from backend 1543b53 — category uniqueness

This commit is contained in:
Siavash Sameni
2026-06-01 17:22:53 +04:00
parent 78707c11a7
commit 02641e1333
5 changed files with 54 additions and 23 deletions

View File

@@ -4,7 +4,7 @@ tags: [data-model, migration, mongodb, postgres, mongoose, helper]
aliases: [Mongo to Postgres, DB Migration Guide, Postgres Migration]
created: 2026-05-31
source: backend/src (automated multi-agent scan)
updated: 2026-06-01 for backend integrate-main-into-development@6df113d
updated: 2026-06-01 for backend integrate-main-into-development@1543b53
---
# MongoDB → PostgreSQL Migration Guide
@@ -17,7 +17,7 @@ updated: 2026-06-01 for backend integrate-main-into-development@6df113d
> **Execution plan:** see the companion [[MongoDB to PostgreSQL Migration Plan (Drizzle)]] for the phased, Drizzle-concrete plan (repository seam, `id_map`, dual-write, per-phase cutover runbook).
> [!warning] Current implementation delta
> This guide started as a migration helper. Backend `integrate-main-into-development@6df113d` now contains the first Postgres implementation layer: Drizzle schemas/migrations through `0008`, `src/db/client.ts`, `id_map`, `pg_dualwrite_gaps`, repository implementations/factory, backfill/verify scripts, conditional `payment_quotes` persistence, and aligned purchase/template request budget validation. Backend `2.8.13` also hardens the PurchaseRequest/SellerOffer backfill runner for marketplace-core dry-runs and selected-offer remapping. The broad service layer is still Mongoose-first and is **not** fully wired through those repositories. Use [[Postgres Runtime Cutover Status]] as the authoritative current-state snapshot.
> This guide started as a migration helper. Backend `integrate-main-into-development@1543b53` now contains the first Postgres implementation layer: Drizzle schemas/migrations through `0009`, `src/db/client.ts`, `id_map`, `pg_dualwrite_gaps`, repository implementations/factory, backfill/verify scripts, conditional `payment_quotes` persistence, and aligned purchase/template request budget validation. Backend `2.8.17` also hardens the PurchaseRequest/SellerOffer backfill runner for marketplace-core dry-runs and selected-offer remapping, and enforces unique active marketplace categories by normalized visible name in Postgres mode. The broad service layer is still Mongoose-first and is **not** fully wired through those repositories. Use [[Postgres Runtime Cutover Status]] as the authoritative current-state snapshot.
> [!info] Scan coverage (2026-05-31)
> - **23** Mongoose models (collections)
@@ -564,8 +564,9 @@ Hierarchical category system for classifying purchase requests and request templ
#### Indexes
- `idx_categories_name` (UNIQUE): optimizes name lookups
- `idx_categories_name` (UNIQUE in Drizzle baseline): optimizes name lookups
- `idx_categories_name_en`: optimizes English name lookups
- `categories_active_name_norm_uq` (UNIQUE, partial): enforces one active category per `lower(btrim(name))`
- `idx_categories_is_active`: heavily used in filtered queries (active categories only)
- `idx_categories_parent_id`: supports tree-building and subcategory queries
@@ -576,6 +577,7 @@ Hierarchical category system for classifying purchase requests and request templ
3. **Timestamps immutability**: Mongoose auto-sets createdAt once; ensure PG constraints prevent manual updates to createdAt.
4. **Cache invalidation**: CategoryService invalidates Redis cache on every mutation; ensure application layer continues to manage this post-migration.
5. **Dual localization**: name (local language) and nameEn (English) are always paired; enforce as a unit in API validation.
6. **Duplicate active labels**: visible category names must be unique after trimming/case-folding. Migration `0009_unique_active_categories.sql` deactivates duplicate active rows and repoints dependent category references before creating the partial unique index.
#### Migration Strategy
@@ -584,7 +586,8 @@ Hierarchical category system for classifying purchase requests and request templ
3. Add composite index on (order, name) to match Mongoose sort patterns: `.sort({ order: 1, name: 1 })`.
4. Migrate all ObjectId values from MongoDB to UUIDs using a UUID generation function or application-layer conversion.
5. Ensure dependent tables (purchase_requests, request_templates) update their category_id FKs to point to new PG uuid PKs.
6. Validate tree structure: no cycles, all parentId refs are valid or NULL.
6. Deactivate duplicate active rows by normalized `name`, and repoint purchase request/category parent references to the kept row before adding `categories_active_name_norm_uq`.
7. Validate tree structure: no cycles, all parentId refs are valid or NULL.
#### Proposed DDL
@@ -604,6 +607,9 @@ CREATE TABLE categories (
CREATE UNIQUE INDEX idx_categories_name ON categories(name);
CREATE INDEX idx_categories_name_en ON categories(name_en);
CREATE UNIQUE INDEX categories_active_name_norm_uq
ON categories (lower(btrim(name)))
WHERE is_active = true;
CREATE INDEX idx_categories_is_active ON categories(is_active);
CREATE INDEX idx_categories_parent_id ON categories(parent_id);
CREATE INDEX idx_categories_order_name ON categories("order", name);
@@ -4385,8 +4391,9 @@ CREATE TRIGGER tg_notification_metadata_validate
| | `order` | B-tree index | `CREATE INDEX idx_levelconfig_order` | Display order |
| | `isActive` | B-tree index | `CREATE INDEX idx_levelconfig_isactive` | Active level filter |
| **ShopSettings** | `sellerId` | UNIQUE | `UNIQUE CONSTRAINT` | One shop per seller |
| **Category** | `name` | B-tree index | `CREATE INDEX idx_category_name` | Name lookup |
| **Category** | `name` | UNIQUE B-tree index | `CREATE UNIQUE INDEX idx_category_name` | Name lookup |
| | `nameEn` | B-tree index | `CREATE INDEX idx_category_name_en` | English name lookup |
| | `lower(btrim(name)) WHERE is_active = true` | Partial UNIQUE expression index | `CREATE UNIQUE INDEX categories_active_name_norm_uq` | Prevent duplicate active visible labels |
| | `isActive` | B-tree index | `CREATE INDEX idx_category_isactive` | Active category filter |
| | `parentId` | B-tree index | `CREATE INDEX idx_category_parentid` | Hierarchy traversal |
| **SellerOffer** | `sellerId` | B-tree index | `CREATE INDEX idx_selleroffer_seller` | Seller's offers |
@@ -4722,7 +4729,7 @@ Each phase has explicit entry/exit criteria. The unit of migration is a bounded
#### Phase 2 — Reference/config data
- Category (self-referential FK, soft-delete), LevelConfig, ConfigSetting, ConfigSettingHistory, ShopSettings, Review. Mostly scalar, low write volume, few invariants. Builds the seed-in-dependency-order path.
- **Entry:** Phase 1 exit. **Exit:** these read from PG; seeds run in PG; FK/unique constraints (e.g., `ShopSettings.sellerId` unique, Category `parentId` ON DELETE SET NULL) enforced.
- **Entry:** Phase 1 exit. **Exit:** these read from PG; seeds run in PG; FK/unique constraints (e.g., `ShopSettings.sellerId` unique, Category `parentId` ON DELETE SET NULL, Category active normalized-name uniqueness) enforced.
#### Phase 3 — User + auth core
- User is the FK hub. Normalize nested `profile`/`preferences`/`points`/`referralStats` and extract the `passkeys` and `refreshTokens` arrays to child tables. Reimplement `toJSON()` password stripping and passkey `default: Date.now()` in app code. Sparse+unique email index → partial unique index `WHERE email IS NOT NULL`.