package processing import ( "context" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) // CategoryBackfillResult is the outcome of BackfillProcessedCategoriesFromMapped. type CategoryBackfillResult struct { Updated int64 `json:"updated"` } // BackfillProcessedCategoriesFromMapped fills empty processed_products.category from // raw_products.mapped_data when a usable unique_id is present. When the company has // categories rows, only known unique_ids are applied (same rule as processOne filter). // Safe for admin "Fix catalog" and migrator reuse. Does not invent categories via vector. func BackfillProcessedCategoriesFromMapped(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (CategoryBackfillResult, error) { var out CategoryBackfillResult if pool == nil { return out, fmt.Errorf("category backfill: nil pool") } if companyID == uuid.Nil { return out, fmt.Errorf("category backfill: empty company id") } ct, err := pool.Exec(ctx, ` WITH src AS ( SELECT p.id AS processed_id, BTRIM(COALESCE( CASE WHEN jsonb_typeof(r.mapped_data->'category') IN ('string', 'number') THEN NULLIF(BTRIM(r.mapped_data->>'category'), '') ELSE NULL END, NULLIF(BTRIM(r.mapped_data->>'category_unique_id'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'unique_id'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'category_unique_id'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'#text'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'id'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'value'), ''), NULLIF(BTRIM(r.mapped_data->'category'->>'code'), '') )) AS cat_raw FROM processed_products p INNER JOIN raw_products r ON r.id = p.raw_product_id AND r.company_id = p.company_id WHERE p.company_id = $1 AND ( COALESCE(NULLIF(BTRIM(p.category), ''), '') = '' OR LOWER(BTRIM(p.category)) = 'none' ) ), mapped AS ( SELECT processed_id, CASE WHEN cat_raw IS NULL OR cat_raw = '' OR LOWER(cat_raw) = 'none' THEN NULL ELSE cat_raw END AS cat_uid FROM src ), eligible AS ( SELECT m.processed_id, m.cat_uid FROM mapped m WHERE m.cat_uid IS NOT NULL AND ( NOT EXISTS (SELECT 1 FROM categories c WHERE c.company_id = $1) OR EXISTS ( SELECT 1 FROM categories c WHERE c.company_id = $1 AND BTRIM(c.unique_id) = m.cat_uid ) ) ) UPDATE processed_products p SET category = e.cat_uid, field_sources = jsonb_set( COALESCE(p.field_sources, '{}'::jsonb), '{category}', '"mapped_backfill"'::jsonb, true ), updated_at = now() FROM eligible e WHERE p.id = e.processed_id`, companyID) if err != nil { return out, fmt.Errorf("category backfill: %w", err) } out.Updated = ct.RowsAffected() return out, nil } // CountProcessedCategoryCoverage returns how many processed rows for companyID have // a non-empty category vs empty/"none". func CountProcessedCategoryCoverage(ctx context.Context, pool *pgxpool.Pool, companyID uuid.UUID) (withCat, withoutCat int64, err error) { if pool == nil { return 0, 0, fmt.Errorf("category coverage: nil pool") } err = pool.QueryRow(ctx, ` SELECT COUNT(*) FILTER ( WHERE COALESCE(NULLIF(BTRIM(category), ''), '') <> '' AND LOWER(BTRIM(category)) <> 'none' ), COUNT(*) FILTER ( WHERE COALESCE(NULLIF(BTRIM(category), ''), '') = '' OR LOWER(BTRIM(category)) = 'none' ) FROM processed_products WHERE company_id = $1`, companyID).Scan(&withCat, &withoutCat) if err != nil { return 0, 0, fmt.Errorf("category coverage: %w", err) } return withCat, withoutCat, nil }