Skip to content

Commit eb02267

Browse files
authored
Merge pull request #239 from webdaad/master
feat: add using to delete and add "SKIP LOCKED" to select
2 parents 4483721 + d5110f9 commit eb02267

4 files changed

Lines changed: 122 additions & 6 deletions

File tree

delete.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const (
1313
deleteMarkerInit injectionMarker = iota
1414
deleteMarkerAfterWith
1515
deleteMarkerAfterDeleteFrom
16+
deleteMarkerAfterUsing
1617
deleteMarkerAfterWhere
1718
deleteMarkerAfterOrderBy
1819
deleteMarkerAfterLimit
@@ -69,6 +70,7 @@ type DeleteBuilder struct {
6970
cteBuilder *CTEBuilder
7071

7172
tables []string
73+
usingTables []string
7274
orderByCols []string
7375
order string
7476
limitVar string
@@ -102,6 +104,18 @@ func (db *DeleteBuilder) DeleteFrom(table ...string) *DeleteBuilder {
102104
return db
103105
}
104106

107+
// Using sets the USING clause in DELETE.
108+
// It's a PostgreSQL extension that allows referencing other tables in the WHERE clause,
109+
// similar to a JOIN.
110+
//
111+
// db.DeleteFrom("orders").Using("customers").Where("orders.customer_id = customers.id", "customers.status = 'inactive'")
112+
// // Generates: DELETE FROM orders USING customers WHERE orders.customer_id = customers.id AND customers.status = 'inactive'
113+
func (db *DeleteBuilder) Using(table ...string) *DeleteBuilder {
114+
db.usingTables = table
115+
db.marker = deleteMarkerAfterUsing
116+
return db
117+
}
118+
105119
// TableNames returns all table names in this DELETE statement.
106120
func (db *DeleteBuilder) TableNames() []string {
107121
var additionalTableNames []string
@@ -264,6 +278,13 @@ func (db *DeleteBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
264278
db.injection.WriteTo(buf, insertMarkerAfterReturning)
265279
}
266280

281+
if len(db.usingTables) > 0 {
282+
buf.WriteLeadingString("USING ")
283+
buf.WriteStrings(db.usingTables, ", ")
284+
}
285+
286+
db.injection.WriteTo(buf, deleteMarkerAfterUsing)
287+
267288
if db.WhereClause != nil {
268289
db.whereClauseProxy.WhereClause = db.WhereClause
269290
defer func() {

delete_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,67 @@ func TestDeleteBuilderReturning(t *testing.T) {
194194
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)
195195
}
196196

197+
func ExampleDeleteBuilder_Using() {
198+
db := NewDeleteBuilder()
199+
db.DeleteFrom("orders")
200+
db.Using("customers")
201+
db.Where(
202+
"orders.customer_id = customers.id",
203+
db.Equal("customers.status", "inactive"),
204+
)
205+
206+
sql, args := db.BuildWithFlavor(PostgreSQL)
207+
fmt.Println(sql)
208+
fmt.Println(args)
209+
210+
// Output:
211+
// DELETE FROM orders USING customers WHERE orders.customer_id = customers.id AND customers.status = $1
212+
// [inactive]
213+
}
214+
215+
func TestDeleteBuilderUsing(t *testing.T) {
216+
a := assert.New(t)
217+
218+
// Single USING table
219+
db := NewDeleteBuilder()
220+
db.DeleteFrom("orders")
221+
db.Using("customers")
222+
db.Where("orders.customer_id = customers.id")
223+
224+
sql, _ := db.BuildWithFlavor(PostgreSQL)
225+
a.Equal("DELETE FROM orders USING customers WHERE orders.customer_id = customers.id", sql)
226+
227+
// Multiple USING tables
228+
db2 := NewDeleteBuilder()
229+
db2.DeleteFrom("orders")
230+
db2.Using("customers", "products")
231+
db2.Where("orders.customer_id = customers.id", "orders.product_id = products.id")
232+
233+
sql, _ = db2.BuildWithFlavor(PostgreSQL)
234+
a.Equal("DELETE FROM orders USING customers, products WHERE orders.customer_id = customers.id AND orders.product_id = products.id", sql)
235+
236+
// USING with RETURNING
237+
db3 := NewDeleteBuilder()
238+
db3.DeleteFrom("orders")
239+
db3.Using("customers")
240+
db3.Where(db3.Equal("customers.id", 42))
241+
db3.Returning("orders.id")
242+
243+
sql, args := db3.BuildWithFlavor(PostgreSQL)
244+
a.Equal("DELETE FROM orders USING customers WHERE customers.id = $1 RETURNING orders.id", sql)
245+
a.Equal([]interface{}{42}, args)
246+
247+
// SQL injection after USING
248+
db4 := NewDeleteBuilder()
249+
db4.DeleteFrom("orders")
250+
db4.Using("customers")
251+
db4.SQL("/* after using */")
252+
db4.Where("orders.customer_id = customers.id")
253+
254+
sql, _ = db4.BuildWithFlavor(PostgreSQL)
255+
a.Equal("DELETE FROM orders USING customers /* after using */ WHERE orders.customer_id = customers.id", sql)
256+
}
257+
197258
func TestDeleteBuilderClone(t *testing.T) {
198259
a := assert.New(t)
199260
cte := With(

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module github.com/huandu/go-sqlbuilder
1+
module github.com/webdaad/go-sqlbuilder
22

33
go 1.18
44

select.go

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ type SelectBuilder struct {
9696
groupByCols []string
9797
orderByCols []string
9898
order string
99-
limitVar string
100-
offsetVar string
101-
forWhat string
99+
limitVar string
100+
offsetVar string
101+
forWhat string
102+
forOf []string
103+
forSkipLocked bool
104+
forNoWait bool
102105

103106
args *Args
104107

@@ -321,15 +324,35 @@ func (sb *SelectBuilder) Offset(offset int) *SelectBuilder {
321324
}
322325

323326
// ForUpdate adds FOR UPDATE at the end of SELECT statement.
324-
func (sb *SelectBuilder) ForUpdate() *SelectBuilder {
327+
// Optional table names are appended as FOR UPDATE OF t1, t2, ...
328+
func (sb *SelectBuilder) ForUpdate(tables ...string) *SelectBuilder {
325329
sb.forWhat = "UPDATE"
330+
sb.forOf = tables
326331
sb.marker = selectMarkerAfterFor
327332
return sb
328333
}
329334

330335
// ForShare adds FOR SHARE at the end of SELECT statement.
331-
func (sb *SelectBuilder) ForShare() *SelectBuilder {
336+
// Optional table names are appended as FOR SHARE OF t1, t2, ...
337+
func (sb *SelectBuilder) ForShare(tables ...string) *SelectBuilder {
332338
sb.forWhat = "SHARE"
339+
sb.forOf = tables
340+
sb.marker = selectMarkerAfterFor
341+
return sb
342+
}
343+
344+
// SkipLocked appends SKIP LOCKED to a FOR UPDATE / FOR SHARE clause.
345+
func (sb *SelectBuilder) SkipLocked() *SelectBuilder {
346+
sb.forSkipLocked = true
347+
sb.forNoWait = false
348+
sb.marker = selectMarkerAfterFor
349+
return sb
350+
}
351+
352+
// NoWait appends NOWAIT to a FOR UPDATE / FOR SHARE clause.
353+
func (sb *SelectBuilder) NoWait() *SelectBuilder {
354+
sb.forNoWait = true
355+
sb.forSkipLocked = false
333356
sb.marker = selectMarkerAfterFor
334357
return sb
335358
}
@@ -569,6 +592,17 @@ func (sb *SelectBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{
569592
buf.WriteLeadingString("FOR ")
570593
buf.WriteString(sb.forWhat)
571594

595+
if len(sb.forOf) > 0 {
596+
buf.WriteString(" OF ")
597+
buf.WriteStrings(sb.forOf, ", ")
598+
}
599+
600+
if sb.forSkipLocked {
601+
buf.WriteString(" SKIP LOCKED")
602+
} else if sb.forNoWait {
603+
buf.WriteString(" NOWAIT")
604+
}
605+
572606
sb.injection.WriteTo(buf, selectMarkerAfterFor)
573607
}
574608

0 commit comments

Comments
 (0)