package db import ( "context" "github.com/pkg/errors" "github.com/uptrace/bun" ) type updater[T any] struct { tx bun.Tx q *bun.UpdateQuery model *T columns []string audit *AuditMeta auditInfo *AuditInfo } // Update creates an updater for a model // You must specify which columns to update via .Column() or use .WherePK() func Update[T any](tx bun.Tx, model *T) *updater[T] { if model == nil { panic("model cannot be nil") } return &updater[T]{ tx: tx, q: tx.NewUpdate().Model(model), model: model, } } // UpdateByID creates an updater with an ID where clause // You must still specify which columns to update via .Column() func UpdateByID[T any](tx bun.Tx, id int, model *T) *updater[T] { if id <= 0 { panic("id must be positive") } return Update(tx, model).Where("id = ?", id) } // Column specifies which columns to update // Example: .Column("start_date", "end_date") func (u *updater[T]) Column(columns ...string) *updater[T] { u.columns = append(u.columns, columns...) u.q = u.q.Column(columns...) return u } // Where adds a WHERE clause // Example: .Where("id = ?", 123) func (u *updater[T]) Where(query string, args ...any) *updater[T] { u.q = u.q.Where(query, args...) return u } // WherePK adds a WHERE clause on the primary key // The model must have its primary key field populated func (u *updater[T]) WherePK() *updater[T] { u.q = u.q.WherePK() return u } // Set adds a raw SET clause for complex updates // Example: .Set("updated_at = NOW()") func (u *updater[T]) Set(query string, args ...any) *updater[T] { u.q = u.q.Set(query, args...) return u } // WithAudit enables audit logging for this update operation // If the provided *AuditInfo is nil, will use reflection to automatically work out the details func (u *updater[T]) WithAudit(meta *AuditMeta, info *AuditInfo) *updater[T] { u.audit = meta u.auditInfo = info return u } // Exec executes the update and optionally logs to audit // Returns an error if update fails or if audit callback fails (triggering rollback) func (u *updater[T]) Exec(ctx context.Context) error { // Build audit details BEFORE update (captures changed fields) var details map[string]any if u.audit != nil && len(u.columns) > 0 { details = extractChangedFields(u.model, u.columns) } // Execute update _, err := u.q.Exec(ctx) if err != nil { return errors.Wrap(err, "bun.UpdateQuery.Exec") } // Handle audit logging if enabled if u.audit != nil { if u.auditInfo == nil { tableName := extractTableName[T]() resourceType := extractResourceType(tableName) action := buildAction(resourceType, "update") u.auditInfo = &AuditInfo{ Action: action, ResourceType: resourceType, ResourceID: extractPrimaryKey(u.model), Details: details, // Changed fields only } } err = LogSuccess(ctx, u.tx, u.audit, u.auditInfo) if err != nil { return errors.Wrap(err, "LogSuccess") } } return nil }