# Code Patterns ## CSV Export (3-layer architecture) Used for pricelist export. **Implementation**: - Handler: `internal/handlers/pricelist.go` — `ExportCSV` - Service: `internal/services/pricelist/service.go` — `StreamItemsForExport` - Repository: `internal/repository/pricelist.go` — `StreamItemsForExport` - Models: `internal/models/pricelist.go` --- ### 1. Handler Layer ```go func (h *PricelistHandler) ExportCSV(c *gin.Context) { id, _ := strconv.ParseUint(c.Param("id"), 10, 32) pl, _ := h.service.GetByID(uint(id)) filename := fmt.Sprintf("pricelist_%s.csv", pl.Version) c.Header("Content-Type", "text/csv; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) // UTF-8 BOM for Excel compatibility c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) writer := csv.NewWriter(c.Writer) writer.Comma = ';' // semicolon for Russian-locale Excel defer writer.Flush() isWarehouse := strings.ToLower(pl.Source) == "warehouse" var header []string if isWarehouse { header = []string{"Article", "Category", "Description", "Available", "Partnumbers", "Price, $", "Settings"} } else { header = []string{"Article", "Category", "Description", "Price, $", "Settings"} } writer.Write(header) h.service.StreamItemsForExport(uint(id), 500, func(items []models.PricelistItem) error { for _, item := range items { writer.Write(buildRow(item, isWarehouse)) } writer.Flush() // flush after each batch return nil }) } ``` ### 2. Service Layer ```go func (s *Service) StreamItemsForExport(id uint, batchSize int, cb func([]models.PricelistItem) error) error { return s.repo.StreamItemsForExport(id, batchSize, cb) } ``` ### 3. Repository Layer ```go func (r *PricelistRepository) StreamItemsForExport(pricelistID uint, batchSize int, callback func([]models.PricelistItem) error) error { offset := 0 for { var items []models.PricelistItem err := r.db.Table("qt_pricelist_items AS pi"). Select("pi.*, COALESCE(l.lot_description, '') AS lot_description"). Joins("LEFT JOIN lot AS l ON l.lot_name = pi.lot_name"). Where("pi.pricelist_id = ?", pricelistID). Order("pi.lot_name"). Offset(offset).Limit(batchSize). Scan(&items).Error if err != nil || len(items) == 0 { break } if isWarehouse { r.enrichWarehouseItems(items) // adds qty, partnumbers } if err := callback(items); err != nil { return err } if len(items) < batchSize { break } offset += batchSize } return nil } ``` --- ## Streaming (avoid loading all into memory) ```go // ❌ BAD: var allItems []Item db.Find(&allItems) // can OOM on millions of rows // ✅ GOOD: for offset := 0; ; offset += batchSize { var batch []Item db.Offset(offset).Limit(batchSize).Find(&batch) processBatch(batch) if len(batch) < batchSize { break } } ``` **Recommended batch size**: 500–1000. --- ## JOIN instead of N+1 ```go // ❌ N+1: for _, item := range items { description := getLotDescription(item.LotName) // N queries } // ✅ JOIN: db.Table("items AS i"). Select("i.*, COALESCE(l.description, '') AS description"). Joins("LEFT JOIN lots AS l ON l.name = i.lot_name") ``` --- ## UTF-8 BOM for Excel ```go // Excel on Windows requires BOM for correct UTF-8 display c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) ``` --- ## Semicolon delimiter for Excel (Russian locale) ```go writer := csv.NewWriter(c.Writer) writer.Comma = ';' ``` --- ## Graceful error handling during streaming ```go // Cannot return JSON error after streaming has started if err != nil { c.String(http.StatusInternalServerError, "Export failed: %v", err) return } ``` --- ## Virtual Fields (GORM) ```go type PricelistItem struct { // Stored fields ID uint `gorm:"primaryKey"` LotName string `gorm:"size:255"` Price float64 `gorm:"type:decimal(12,2)"` // Virtual: populated via JOIN LotDescription string `gorm:"-:migration" json:"lot_description,omitempty"` Category string `gorm:"-:migration" json:"category,omitempty"` // Virtual: populated programmatically AvailableQty *float64 `gorm:"-" json:"available_qty,omitempty"` Partnumbers []string `gorm:"-" json:"partnumbers,omitempty"` } ``` - `gorm:"-:migration"` — no DB column, but mapped on SELECT. - `gorm:"-"` — fully ignored in all DB operations. --- ## CSV Export Checklist - [ ] HTTP headers: `Content-Type`, `Content-Disposition` - [ ] UTF-8 BOM `{0xEF, 0xBB, 0xBF}` - [ ] Semicolon delimiter `;` for Russian-locale Excel - [ ] Streaming with batch processing (no full load into memory) - [ ] JOIN to avoid N+1 queries - [ ] `writer.Flush()` after each batch - [ ] Graceful error handling (no JSON after streaming starts) - [ ] Dynamic headers when needed - [ ] Conditional enrichment (warehouse vs estimate)