Soft delete¶
WithSoftDeletion(fn) turns a physical DELETE into an UPDATE of selected fields. The "mark, don't drop" pattern — useful to preserve data and keep foreign keys intact.
Setup¶
.Columns(func(m *User, c *gerpo.ColumnBuilder[User]) {
c.Field(&m.ID)
c.Field(&m.DeletedAt).OmitOnInsert()
}).
WithSoftDeletion(func(m *User, b *gerpo.SoftDeletionBuilder[User]) {
b.Field(&m.DeletedAt).SetValueFn(func(ctx context.Context) any {
t := time.Now().UTC()
return &t
})
}).
WithQuery(func(m *User, h query.PersistentHelper[User]) {
h.Where().Field(&m.DeletedAt).EQ(nil)
})
Three required pieces:
- Marker column (
DeletedAt). Typically nullable —*time.Time. AddOmitOnInsertso it can't be accidentally written at INSERT time. WithSoftDeletion— describes the value to write on "delete". The function runs on everyDeletecall and receives the context (useful for user/clock/tenant).WithQuerywith a filter — so soft-deleted records don't leak into SELECTs. Without it they show up in listings.
SetValueFn return type
The returned value must match the field type — for *time.Time return *time.Time, not time.Time. Build() runs a type probe: each SetValueFn is invoked once with context.Background() and the returned value is checked against the field type. A mismatch (or a panic from inside the callback) is reported from Build() rather than crashing on the first soft Delete() call.
This is not a full compile-time check — the callback body still runs at repo-build time, and if it branches on ctx values (ctx.Value(tenantKey)) that context.Background() does not carry, the probe exercises a different path than production. Keep SetValueFn bodies free of ctx-dependent branches when possible, or accept that the probe only catches the common ctx=Background case.
How it works¶
repo.Delete(ctx, …) executes
instead of DELETE FROM users WHERE …. It returns the UPDATE's RowsAffected. If zero rows match, it returns ErrNotFound.
Restoration¶
There's no dedicated API — restore a row with a direct UPDATE bypassing the repo:
Alternatively, you can run an extra repo.Update with Only(&m.DeletedAt) and a nil value in the model — but that bypasses the persistent WHERE, so it only works when the repo's structure allows it.
Multiple marker fields¶
SoftDeletionBuilder supports multiple Field calls — all of them will be updated on soft-delete: