# Repo Layer Agent Instructions

## Overview

Repos handle **data persistence** using GORM. They encapsulate all database operations and use functional options for flexible filtering.

## Package Structure

```
repo/
├── entity/                    # Entity definitions
│   ├── entity.go             # Main entities (gen:qs comment)
│   ├── autogenerated_entity.go  # DO NOT EDIT - generated QuerySets
│   ├── base.go               # Base types (ID, Updatable, Versioned)
│   ├── constant.go           # Enum constants
│   └── versioning.go         # CAS/optimistic locking
├── <feature>/                # Feature-specific repos
│   ├── <feature>.go          # Interface + FilterOp types
│   └── <feature>_impl.go     # Implementation
├── fakedb.go                 # Test database setup
└── impl/                     # Shared implementations
```

## Entity Pattern

**Define in `entity/entity.go`:**
```go
//go:generate ../../../tools/bin/goqueryset -in entity.go

// gen:qs  ← This comment triggers QuerySet generation
type (
    Backup struct {
        Updatable              // ID, CreatedAt, UpdatedAt
        Versioned              // Optimistic locking
        DeletedAt *time.Time   `gorm:"type:TIMESTAMP;deleted_at"`
        
        // Foreign keys
        ClusterID entity.ID    `gorm:"type:bigint(20) unsigned;not null;index"`
        
        // Enums
        Status BackupStatus    `gorm:"type:ENUM('pending','running','success','failed');not null"`
        
        // JSON fields
        Config *BackupConfig   `gorm:"type:JSON;serializer:json"`
    }
)
```

**Enrich entity with methods in `entity/xxx_ext.go`:**

Use **rich domain model** rather than anemic model. Extension files add behavior to entities without modifying generated code.

**When to use extension methods:**
1. **Domain logic** - Business rules, status checks, validation
2. **Computed properties** - Derived values, name generation
3. **Type conversions** - Safe type casting, parsing with error handling
4. **Encapsulation** - Hide complex implementation details
5. **Business operations** - State transitions, workflows

**Common patterns:**

```go
// Name/identifier generation
func (b *Backup) CRName() string {
    if b.OldBackupName != "" {
        return b.OldBackupName
    }
    return fmt.Sprintf("%s-%d", strings.ToLower(b.BackupSchedule.Name), b.ID)
}

// State transitions
func (b *Backup) DeleteBackup(deletedAt time.Time) bizerr.BizError {
    if b.Status == BackupStatusRunning || b.Status == BackupStatusPending {
        b.Status = BackupStatusTerminated
    }
    b.DeletedAt = &deletedAt
    return nil
}
```

**Best practices:**
- **No database access** - Extension methods are pure business logic
- **Pass dependencies** - Don't access singletons; accept params (e.g., `encryptor`)
- **Return errors** - Use `bizerr.BizError` for operations that can fail
- **Context when needed** - Accept `context.Context` for logging/tracing

**GORM tag reference:**
| Tag | Purpose |
|-----|---------|
| `type:VARCHAR(255)` | Column type |
| `not null` | NOT NULL constraint |
| `default:false` | Default value |
| `index` | Simple index |
| `index:idx_name,priority:1` | Composite index |
| `uniqueIndex` | Unique constraint |
| `type:JSON;serializer:json` | JSON column |
| `<-:create` | Write only on create |

## Interface Pattern

**Define in `<feature>/<feature>.go`:**
```go
package backup

import (
    "context"
    "github.com/tidbcloud/dataflow-service/app/repo/entity"
    "github.com/tidbcloud/pkgv2/bizerr"
)

// FilterOp type for functional options pattern
type BackupFilterOp func(qs entity.BackupQuerySet) entity.BackupQuerySet

type BackupRepo interface {
    // Single entity operations
    Load(ctx context.Context, id uint64) (*entity.Backup, bizerr.BizError)
    Create(ctx context.Context, backup *entity.Backup) bizerr.BizError
    Save(ctx context.Context, backup *entity.Backup) bizerr.BizError
    
    // Functional options queries
    Get(ctx context.Context, ops ...BackupFilterOp) (*entity.Backup, bizerr.BizError)
    List(ctx context.Context, ops ...BackupFilterOp) ([]entity.Backup, bizerr.BizError)
    Count(ctx context.Context, ops ...BackupFilterOp) (int64, bizerr.BizError)
}
```

## Implementation Pattern

```go
package backup

import "github.com/tidbcloud/pkgv2/db"

var _ BackupRepo = (*BackupRepoImpl)(nil) // Compile-time check

type BackupRepoImpl struct {
    dbProvider db.Provider
}

func NewBackupRepo(dbProvider db.Provider) BackupRepo {
    return &BackupRepoImpl{dbProvider: dbProvider}
}

// Load by ID
func (r *BackupRepoImpl) Load(ctx context.Context, id uint64) (*entity.Backup, bizerr.BizError) {
    rdb := r.dbProvider.ReadDB(ctx)  // Use read replica
    var backup entity.Backup
    if err := rdb.Where("id = ?", id).First(&backup).Error; err != nil {
        return nil, bizerr.WrapGormError(err, "load backup %d", id)
    }
    return &backup, nil
}

// Save with versioning (optimistic lock)
func (r *BackupRepoImpl) Save(ctx context.Context, backup *entity.Backup) bizerr.BizError {
    wdb := r.dbProvider.WriteDB(ctx)  // Use primary
    if err := entity.SaveWithVersioning(wdb, backup); err != nil {
        return err.Chain("save backup %d", backup.ID)
    }
    return nil
}

// List with functional options
func (r *BackupRepoImpl) List(ctx context.Context, ops ...BackupFilterOp) ([]entity.Backup, bizerr.BizError) {
    rdb := r.dbProvider.ReadDB(ctx)
    qs := entity.NewBackupQuerySet(rdb)
    for _, op := range ops {
        qs = op(qs)
    }
    var backups []entity.Backup
    if err := qs.All(&backups); err != nil {
        return nil, bizerr.WrapGormError(err, "list backups")
    }
    return backups, nil
}

// Count (reset offset/limit first!)
func (r *BackupRepoImpl) Count(ctx context.Context, ops ...BackupFilterOp) (int64, bizerr.BizError) {
    rdb := r.dbProvider.ReadDB(ctx)
    qs := entity.NewBackupQuerySet(rdb)
    for _, op := range ops {
        qs = op(qs)
    }
    count, err := qs.Offset(-1).Limit(-1).Count()  // Important: reset pagination
    if err != nil {
        return 0, bizerr.WrapGormError(err, "count backups")
    }
    return count, nil
}
```

## QuerySet Methods (Generated. It's NOT recommended, use GORM directly.)

The `goqueryset` tool generates type-safe methods:

```go
// Create QuerySet
qs := entity.NewBackupQuerySet(db)

// Filters
qs.ClusterIDEq(id)           // WHERE cluster_id = ?
qs.ClusterIDIn(ids...)       // WHERE cluster_id IN (?)
qs.ClusterIDNe(id)           // WHERE cluster_id != ?
qs.StatusIn(statuses...)     // WHERE status IN (?)
qs.DeletedAtIsNull()         // WHERE deleted_at IS NULL

// Ordering
qs.OrderDescByCreatedAt()    // ORDER BY created_at DESC

// Pagination
qs.Offset(10).Limit(20)

// Preloading
qs.PreloadBackupSchedule()

// Execution
qs.One(&backup)              // Single result
qs.All(&backups)             // All results
qs.Count()                   // Count
```

## Versioning (Optimistic Lock)

```go
// SaveWithVersioning does CAS update
// Fails with ErrCodeOptimisticLockConflict if version changed
if err := entity.SaveWithVersioning(wdb, backup); err != nil {
    if err.ErrCode() == bizerr.ErrCodeOptimisticLockConflict {
        // Handle conflict - reload and retry
    }
    return err
}

// Retry pattern for CAS updates
err := entity.RetrySaveWithVersioning(wdb,
    func(wdb *gorm.DB) (interface{}, error) { return loadLatest(wdb, id) },
    func(latest interface{}) (interface{}, error) { 
        b := latest.(*entity.Backup)
        b.Status = newStatus
        return b, nil
    },
    3,              // retryTimes
    time.Second,    // retryInterval
)
```

## Soft Delete

```go
func (r *BackupRepoImpl) SoftDelete(ctx context.Context, id entity.ID) bizerr.BizError {
    wdb := r.dbProvider.WriteDB(ctx)
    err := wdb.Model(&entity.Backup{}).
        Where("id = ? AND deleted_at IS NULL", id).
        Update("deleted_at", time.Now()).
        Error
    return bizerr.WrapGormError(err, "soft delete backup %d", id)
}
```

## Patch Update (Bypass Versioning)

```go
func (r *BackupRepoImpl) Patch(ctx context.Context, id entity.ID, patches map[string]interface{}) bizerr.BizError {
    wdb := r.dbProvider.WriteDB(ctx)
    err := wdb.Model(&entity.Backup{}).Where("id = ?", id).Updates(patches).Error
    return bizerr.WrapGormError(err, "patch backup %d", id)
}
```

## Testing Pattern

```go
func (s *repoTestSuite) SetupSuite() {
    db, err := repo.GetFakeDB()
    s.Require().NoError(err)
    s.db = db
    s.repo = NewBackupRepo(db.NewProvider(db))
}

func (s *repoTestSuite) TestList() {
    tests := map[string]struct {
        ops       []BackupFilterOp
        expectLen int
    }{
        "filter by cluster": {
            ops: []BackupFilterOp{
                func(qs entity.BackupQuerySet) entity.BackupQuerySet {
                    return qs.ClusterIDEq(123)
                },
            },
            expectLen: 3,
        },
    }
    
    for name, tc := range tests {
        s.Run(name, func() {
            results, err := s.repo.List(context.Background(), tc.ops...)
            s.NoError(err)
            s.Len(results, tc.expectLen)
        })
    }
}
```

## Common Pitfalls

1. **Always use ReadDB/WriteDB** - Never use raw `*gorm.DB` directly
2. **Reset pagination for Count** - Use `qs.Offset(-1).Limit(-1).Count()`
3. **Wrap GORM errors** - Always use `bizerr.WrapGormError()`
4. **Use SaveWithVersioning** - For entities with `Versioned` embedded
