Upsert and merge
Upsert inserts a row, or follows your instructions if a unique constraint says it's already there.
Upsert
user := &User{
Name: "Ada",
Email: "ada@example.com",
Age: 36,
}
wrote, err := db.Users.Upsert(ctx, user, ant.OnConflict(&U.Email))
By default, every column except the key and the constraint's own is overwritten, and your struct receives the stored row, key included. A zero value overwrites too: no guessing here either.
Choosing what changes
DoUpdate names the columns to overwrite, and Set assigns values, including ones based on the existing row.
onEmail := ant.OnConflict(&U.Email)
wrote, err := db.Users.Upsert(ctx, user, onEmail.DoUpdate(ant.F(&U.Age)))
wrote, err = db.Users.Upsert(ctx, user, onEmail.Set(ant.Inc(&U.Age, 1)))
Skipping existing rows
DoNothing keeps the row and reports false, leaving your struct as it was.
wrote, err := db.Users.Upsert(ctx, user, ant.OnConflict(&U.Email).DoNothing())
A key on several columns
And adds a column, for a constraint on more than one.
conflict := ant.OnConflict(&U.Email).And(ant.F(&U.Name))
Many rows
InsertMany takes a conflict too.
n, err := db.Users.InsertMany(ctx, users, ant.WithConflict(ant.OnConflict(&U.Email).DoNothing()))
ON DUPLICATE KEY has no target, so any unique key triggers it, not only the columns you name. If a different key is the one that collides, Upsert can't find the row by your columns, and reports that it wrote nothing. Name the only unique key that can collide. SQL Server has no upsert: use ant.Merge.
Merge
ant.Merge decides, row by row, what a batch of source rows does to a table. One statement can update, insert, and delete, which Upsert can't. Clauses run in the order you add them, and the first one that fits a row wins. It needs PostgreSQL 15, or SQL Server.
type Stock struct {
ID int64
SKU string `ant:"unique"`
Count int
}
type Staged struct {
ID int64
SKU string
Count int
}
var (
ST = ant.Fields[Stock]()
SG = ant.Fields[Staged]()
)
changed, err := ant.Merge(&db.Stock, db.Staged.Query(), ant.EqCol(&ST.SKU, &SG.SKU)).
WhenMatchedIf(ant.Eq(&SG.Count, 0)).Delete().
WhenMatched(ant.SetExpr(&ST.Count, ant.Col(&SG.Count))).
WhenNotMatched(ant.Into(&ST.SKU, &SG.SKU), ant.Into(&ST.Count, &SG.Count)).
Exec(ctx)
Here a sold-out item is deleted, a known one is updated, and a new one is inserted.
See it tested: tests/features/writing/upsert_test.go