Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
deleteMarkerInit injectionMarker = iota
deleteMarkerAfterWith
deleteMarkerAfterDeleteFrom
deleteMarkerAfterUsing
deleteMarkerAfterWhere
deleteMarkerAfterOrderBy
deleteMarkerAfterLimit
Expand Down Expand Up @@ -69,6 +70,7 @@ type DeleteBuilder struct {
cteBuilder *CTEBuilder

tables []string
usingTables []string
orderByCols []string
order string
limitVar string
Expand Down Expand Up @@ -102,6 +104,18 @@ func (db *DeleteBuilder) DeleteFrom(table ...string) *DeleteBuilder {
return db
}

// Using sets the USING clause in DELETE.
// It's a PostgreSQL extension that allows referencing other tables in the WHERE clause,
// similar to a JOIN.
//
// db.DeleteFrom("orders").Using("customers").Where("orders.customer_id = customers.id", "customers.status = 'inactive'")
// // Generates: DELETE FROM orders USING customers WHERE orders.customer_id = customers.id AND customers.status = 'inactive'
func (db *DeleteBuilder) Using(table ...string) *DeleteBuilder {
db.usingTables = table
db.marker = deleteMarkerAfterUsing
return db
}

// TableNames returns all table names in this DELETE statement.
func (db *DeleteBuilder) TableNames() []string {
var additionalTableNames []string
Expand Down Expand Up @@ -264,6 +278,13 @@ func (db *DeleteBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
db.injection.WriteTo(buf, insertMarkerAfterReturning)
}

if len(db.usingTables) > 0 {
buf.WriteLeadingString("USING ")
buf.WriteStrings(db.usingTables, ", ")
}

db.injection.WriteTo(buf, deleteMarkerAfterUsing)

if db.WhereClause != nil {
db.whereClauseProxy.WhereClause = db.WhereClause
defer func() {
Expand Down
61 changes: 61 additions & 0 deletions delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,67 @@ func TestDeleteBuilderReturning(t *testing.T) {
a.Equal("WITH temp_user AS (SELECT id FROM inactive_users) DELETE FROM user, temp_user WHERE user.id IN (SELECT id FROM temp_user) RETURNING id, deleted_at", sql)
}

func ExampleDeleteBuilder_Using() {
db := NewDeleteBuilder()
db.DeleteFrom("orders")
db.Using("customers")
db.Where(
"orders.customer_id = customers.id",
db.Equal("customers.status", "inactive"),
)

sql, args := db.BuildWithFlavor(PostgreSQL)
fmt.Println(sql)
fmt.Println(args)

// Output:
// DELETE FROM orders USING customers WHERE orders.customer_id = customers.id AND customers.status = $1
// [inactive]
}

func TestDeleteBuilderUsing(t *testing.T) {
a := assert.New(t)

// Single USING table
db := NewDeleteBuilder()
db.DeleteFrom("orders")
db.Using("customers")
db.Where("orders.customer_id = customers.id")

sql, _ := db.BuildWithFlavor(PostgreSQL)
a.Equal("DELETE FROM orders USING customers WHERE orders.customer_id = customers.id", sql)

// Multiple USING tables
db2 := NewDeleteBuilder()
db2.DeleteFrom("orders")
db2.Using("customers", "products")
db2.Where("orders.customer_id = customers.id", "orders.product_id = products.id")

sql, _ = db2.BuildWithFlavor(PostgreSQL)
a.Equal("DELETE FROM orders USING customers, products WHERE orders.customer_id = customers.id AND orders.product_id = products.id", sql)

// USING with RETURNING
db3 := NewDeleteBuilder()
db3.DeleteFrom("orders")
db3.Using("customers")
db3.Where(db3.Equal("customers.id", 42))
db3.Returning("orders.id")

sql, args := db3.BuildWithFlavor(PostgreSQL)
a.Equal("DELETE FROM orders USING customers WHERE customers.id = $1 RETURNING orders.id", sql)
a.Equal([]interface{}{42}, args)

// SQL injection after USING
db4 := NewDeleteBuilder()
db4.DeleteFrom("orders")
db4.Using("customers")
db4.SQL("/* after using */")
db4.Where("orders.customer_id = customers.id")

sql, _ = db4.BuildWithFlavor(PostgreSQL)
a.Equal("DELETE FROM orders USING customers /* after using */ WHERE orders.customer_id = customers.id", sql)
}

func TestDeleteBuilderClone(t *testing.T) {
a := assert.New(t)
cte := With(
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/huandu/go-sqlbuilder
module github.com/webdaad/go-sqlbuilder

go 1.18

Expand Down
44 changes: 39 additions & 5 deletions select.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ type SelectBuilder struct {
groupByCols []string
orderByCols []string
order string
limitVar string
offsetVar string
forWhat string
limitVar string
offsetVar string
forWhat string
forOf []string
forSkipLocked bool
forNoWait bool

args *Args

Expand Down Expand Up @@ -321,15 +324,35 @@ func (sb *SelectBuilder) Offset(offset int) *SelectBuilder {
}

// ForUpdate adds FOR UPDATE at the end of SELECT statement.
func (sb *SelectBuilder) ForUpdate() *SelectBuilder {
// Optional table names are appended as FOR UPDATE OF t1, t2, ...
func (sb *SelectBuilder) ForUpdate(tables ...string) *SelectBuilder {
sb.forWhat = "UPDATE"
sb.forOf = tables
sb.marker = selectMarkerAfterFor
return sb
}

// ForShare adds FOR SHARE at the end of SELECT statement.
func (sb *SelectBuilder) ForShare() *SelectBuilder {
// Optional table names are appended as FOR SHARE OF t1, t2, ...
func (sb *SelectBuilder) ForShare(tables ...string) *SelectBuilder {
sb.forWhat = "SHARE"
sb.forOf = tables
sb.marker = selectMarkerAfterFor
return sb
}

// SkipLocked appends SKIP LOCKED to a FOR UPDATE / FOR SHARE clause.
func (sb *SelectBuilder) SkipLocked() *SelectBuilder {
sb.forSkipLocked = true
sb.forNoWait = false
sb.marker = selectMarkerAfterFor
return sb
}

// NoWait appends NOWAIT to a FOR UPDATE / FOR SHARE clause.
func (sb *SelectBuilder) NoWait() *SelectBuilder {
sb.forNoWait = true
sb.forSkipLocked = false
sb.marker = selectMarkerAfterFor
return sb
}
Expand Down Expand Up @@ -569,6 +592,17 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
buf.WriteLeadingString("FOR ")
buf.WriteString(sb.forWhat)

if len(sb.forOf) > 0 {
buf.WriteString(" OF ")
buf.WriteStrings(sb.forOf, ", ")
}

if sb.forSkipLocked {
buf.WriteString(" SKIP LOCKED")
} else if sb.forNoWait {
buf.WriteString(" NOWAIT")
}

sb.injection.WriteTo(buf, selectMarkerAfterFor)
}

Expand Down
Loading