From 508cada040f7a6d2a9f15a8d35945f7055edbead Mon Sep 17 00:00:00 2001 From: Istvan Kiss Date: Mon, 19 Jan 2026 16:05:57 +0100 Subject: [PATCH 1/3] added From support for UpdateBuilder --- update.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/update.go b/update.go index 124d6f6..d469ddd 100644 --- a/update.go +++ b/update.go @@ -15,6 +15,7 @@ const ( updateMarkerAfterWith updateMarkerAfterUpdate updateMarkerAfterSet + updateMarkerAfterFrom updateMarkerAfterWhere updateMarkerAfterOrderBy updateMarkerAfterLimit @@ -71,6 +72,7 @@ type UpdateBuilder struct { cteBuilder *CTEBuilder tables []string + fromTables []string assignments []string orderByCols []string order string @@ -140,6 +142,13 @@ func (ub *UpdateBuilder) SetMore(assignment ...string) *UpdateBuilder { return ub } +// From sets table names of FROM in UPDATE. +func (ub *UpdateBuilder) From(table ...string) *UpdateBuilder { + ub.fromTables = table + ub.marker = updateMarkerAfterFrom + return ub +} + // Where sets expressions of WHERE in UPDATE. func (ub *UpdateBuilder) Where(andExpr ...string) *UpdateBuilder { if len(andExpr) == 0 || estimateStringsBytes(andExpr) == 0 { @@ -336,7 +345,7 @@ func (ub *UpdateBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{ buf.WriteStringsPrefixed("INSERTED.", ub.returning, ", ") } - ub.injection.WriteTo(buf, insertMarkerAfterReturning) + ub.injection.WriteTo(buf, updateMarkerAfterReturning) } if flavor != MySQL { @@ -351,6 +360,20 @@ func (ub *UpdateBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{ } } + if flavor == PostgreSQL || flavor == SQLite || flavor == SQLServer { + if len(ub.fromTables) > 0 { + + if ub.cteBuilder == nil || len(ub.cteBuilder.tableNamesForFrom()) == 0 { + buf.WriteLeadingString("FROM ") + } else { + buf.WriteString(", ") + } + + buf.WriteStrings(ub.fromTables, ", ") + ub.injection.WriteTo(buf, updateMarkerAfterFrom) + } + } + if ub.WhereClause != nil { ub.whereClauseProxy.WhereClause = ub.WhereClause defer func() { From a8ee7d222bd4d225fe3d8249099ff0a17982f5fe Mon Sep 17 00:00:00 2001 From: Istvan Kiss Date: Mon, 19 Jan 2026 16:06:13 +0100 Subject: [PATCH 2/3] test From on UpdateBuilder --- update_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/update_test.go b/update_test.go index 29c3428..15c8c76 100644 --- a/update_test.go +++ b/update_test.go @@ -283,3 +283,95 @@ func TestUpdateBuilderClone(t *testing.T) { clone.Asc().Limit(5) a.NotEqual(ub.String(), clone.String()) } + +func TestUpdateBuilderFrom(t *testing.T) { + a := assert.New(t) + ub := NewUpdateBuilder() + ub.Update("user") + ub.Set(ub.Assign("name", "Huan Du")) + ub.From("person") + ub.Where(ub.Equal("id", 123)) + + sql, _ := ub.BuildWithFlavor(MySQL) + a.Equal("UPDATE user SET name = ? WHERE id = ?", sql) + + sql, _ = ub.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET name = $1 FROM person WHERE id = $2", sql) + + sql, _ = ub.BuildWithFlavor(SQLite) + a.Equal("UPDATE user SET name = ? FROM person WHERE id = ?", sql) + + sql, _ = ub.BuildWithFlavor(SQLServer) + a.Equal("UPDATE user SET name = @p1 FROM person WHERE id = @p2", sql) + + sql, _ = ub.BuildWithFlavor(CQL) + a.Equal("UPDATE user SET name = ? WHERE id = ?", sql) + + sql, _ = ub.BuildWithFlavor(ClickHouse) + a.Equal("UPDATE user SET name = ? WHERE id = ?", sql) + + sql, _ = ub.BuildWithFlavor(Presto) + a.Equal("UPDATE user SET name = ? WHERE id = ?", sql) + + // Test with no from + ub2 := NewUpdateBuilder() + ub2.Update("user") + ub2.Set(ub2.Assign("name", "Test")) + ub2.From() + ub2.Where(ub2.Equal("id", 1)) + + sql, _ = ub2.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET name = $1 WHERE id = $2", sql) + + // Test with multiple from tables + ub3 := NewUpdateBuilder() + ub3.Update("user") + ub3.Set(ub3.Assign("name", "Test")) + ub3.From("person", "company") + ub3.Where(ub3.Equal("id", 1)) + + sql, _ = ub3.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET name = $1 FROM person, company WHERE id = $2", sql) + + // Test chaining + ub5 := NewUpdateBuilder().Update("user").Set("status = 1").From("person").From("company") + sql, _ = ub5.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET status = 1 FROM company", sql) // Last From call overwrites + + // Test SQL injection after FROM + ub6 := NewUpdateBuilder() + ub6.Update("user") + ub6.Set(ub6.Assign("name", "Test")) + ub6.From("person") + ub6.SQL("/* comment after from */") + ub6.Where(ub6.Equal("id", 1)) + + sql, _ = ub6.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET name = $1 FROM person /* comment after from */ WHERE id = $2", sql) + + // Test with CTE (WITH clause) + cte := With(CTETable("temp_user").As(Select("id").From("active_users"))) + ub7 := cte.Update("user") + ub7.Set(ub7.Assign("status", "active")) + ub7.From("person") + ub7.Where("user.id IN (SELECT id FROM temp_user)") + + sql, _ = ub7.BuildWithFlavor(PostgreSQL) + a.Equal("WITH temp_user AS (SELECT id FROM active_users) UPDATE user SET status = $1 FROM temp_user, person WHERE user.id IN (SELECT id FROM temp_user)", sql) + + // Test with SQLServer Returning + ub8 := ub.Clone().Returning("id", "name") + sql, _ = ub8.BuildWithFlavor(SQLServer) + a.Equal("UPDATE user SET name = @p1 OUTPUT INSERTED.id, INSERTED.name FROM person WHERE id = @p2", sql) + + // Test with SQL injection after WHERE + ub9 := NewUpdateBuilder() + ub9.Update("user") + ub9.Set(ub9.Assign("name", "Test")) + ub9.From("person") + ub9.Where("user.id = person.id") + ub9.SQL("/* comment after where */") + + sql, _ = ub9.BuildWithFlavor(PostgreSQL) + a.Equal("UPDATE user SET name = $1 FROM person WHERE user.id = person.id /* comment after where */", sql) +} From 42226601f13131012ba9cec65c86acd4328c06ec Mon Sep 17 00:00:00 2001 From: Istvan Kiss Date: Mon, 19 Jan 2026 16:06:36 +0100 Subject: [PATCH 3/3] document UpdateBuilder.From in README.md --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 9a1ab5f..17d0b89 100644 --- a/README.md +++ b/README.md @@ -203,6 +203,26 @@ fmt.Println(ub) // UPDATE users SET level = level + ? WHERE id = ? ``` +### Build `UPDATE ... FROM` + +`UpdateBuilder.From` emits a `FROM` clause for PostgreSQL, SQLite, and SQLServer flavors (it is ignored by other flavors). When a CTE includes tables created with `CTETable`, those table names are emitted before any explicit `From(...)` tables. + +```go +ub := PostgreSQL.NewUpdateBuilder() +ub.Update("users") +ub.Set(ub.Assign("name", "Huan Du")) +ub.From("people") +ub.Where("users.person_id = people.id") + +sql, args := ub.Build() +fmt.Println(sql) +fmt.Println(args) + +// Output: +// UPDATE users SET name = $1 FROM people WHERE users.person_id = people.id +// [Huan Du] +``` + Refer to the [WhereClause](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#WhereClause) examples to learn its usage. ### Build `ORDER BY` clause