Skip to content

Commit a25e94c

Browse files
dialect/sql: added support for batch delete query
1 parent 27ab057 commit a25e94c

File tree

2 files changed

+122
-31
lines changed

2 files changed

+122
-31
lines changed

dialect/sql/builder.go

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -819,9 +819,10 @@ type DeleteBuilder struct {
819819
schema string
820820
where *Predicate
821821

822-
// For YDB's DELETE FROM ... ON SELECT pattern
823-
onSelect *Selector
822+
// YDB-specific:
823+
onSelect *Selector // DELETE FROM ... ON SELECT pattern
824824
returning []string
825+
isBatch bool // use BATCH DELETE instead of DELETE
825826
}
826827

827828
// Delete creates a builder for the `DELETE` statement.
@@ -839,6 +840,18 @@ type DeleteBuilder struct {
839840
// )
840841
func Delete(table string) *DeleteBuilder { return &DeleteBuilder{table: table} }
841842

843+
// BatchDelete creates a builder for the `BATCH DELETE FROM` statement (YDB-specific).
844+
// BATCH DELETE processes large tables in batches, minimizing lock invalidation risk.
845+
//
846+
// Note: BATCH DELETE is only supported in YDB dialect.
847+
//
848+
// BatchDelete("/local/my_table")
849+
//
850+
// .Where(GT("Key1", 1))
851+
func BatchDelete(table string) *DeleteBuilder {
852+
return &DeleteBuilder{table: table, isBatch: true}
853+
}
854+
842855
// Schema sets the database name for the table whose row will be deleted.
843856
func (d *DeleteBuilder) Schema(name string) *DeleteBuilder {
844857
d.schema = name
@@ -891,8 +904,32 @@ func (d *DeleteBuilder) Returning(columns ...string) *DeleteBuilder {
891904

892905
// Query returns query representation of a `DELETE` statement.
893906
func (d *DeleteBuilder) Query() (string, []any) {
907+
query, args, _ := d.QueryErr()
908+
return query, args
909+
}
910+
911+
func (d *DeleteBuilder) QueryErr() (string, []any, error) {
894912
b := d.Builder.clone()
895-
b.WriteString("DELETE FROM ")
913+
914+
// BATCH DELETE (YDB-specific)
915+
if d.isBatch {
916+
if !b.ydb() {
917+
b.AddError(fmt.Errorf("BATCH DELETE: unsupported dialect: %q", b.dialect))
918+
return "", nil, b.Err()
919+
}
920+
if len(d.returning) > 0 {
921+
b.AddError(fmt.Errorf("BATCH DELETE: RETURNING clause is not supported"))
922+
return "", nil, b.Err()
923+
}
924+
if d.onSelect != nil {
925+
b.AddError(fmt.Errorf("BATCH DELETE: DELETE ON pattern is not supported"))
926+
return "", nil, b.Err()
927+
}
928+
b.WriteString("BATCH DELETE FROM ")
929+
} else {
930+
b.WriteString("DELETE FROM ")
931+
}
932+
896933
b.writeSchema(d.schema)
897934
b.Ident(d.table)
898935

@@ -907,7 +944,7 @@ func (d *DeleteBuilder) Query() (string, []any) {
907944
}
908945

909946
joinReturning(d.returning, &b)
910-
return b.String(), b.args
947+
return b.String(), b.args, nil
911948
}
912949

913950
// Predicate is a where predicate.
@@ -3772,6 +3809,18 @@ func (d *DialectBuilder) Delete(table string) *DeleteBuilder {
37723809
return b
37733810
}
37743811

3812+
// BatchDelete creates a DeleteBuilder for the BATCH DELETE statement with the configured dialect.
3813+
// BATCH DELETE is only supported in YDB dialect.
3814+
//
3815+
// Dialect(dialect.YDB).
3816+
// BatchDelete("users").
3817+
// Where(GT("Key1", 1))
3818+
func (d *DialectBuilder) BatchDelete(table string) *DeleteBuilder {
3819+
b := BatchDelete(table)
3820+
b.SetDialect(d.dialect)
3821+
return b
3822+
}
3823+
37753824
// Select creates a Selector for the configured dialect.
37763825
//
37773826
// Dialect(dialect.Postgres).

dialect/sql/builder_test.go

Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2297,40 +2297,67 @@ func TestBatchUpdate_YDB(t *testing.T) {
22972297
}, args)
22982298
})
22992299

2300-
t.Run("BATCH UPDATE without WHERE clause", func(t *testing.T) {
2300+
t.Run("BATCH UPDATE on non-YDB dialect should error", func(t *testing.T) {
2301+
builder := Dialect(dialect.MySQL).
2302+
BatchUpdate("users").
2303+
Set("status", "active").
2304+
Where(GT("created_at", "2024-01-01"))
2305+
2306+
query, args, err := builder.QueryErr()
2307+
2308+
require.Empty(t, query)
2309+
require.Empty(t, args)
2310+
require.Error(t, err)
2311+
})
2312+
2313+
t.Run("BATCH UPDATE with RETURNING should error", func(t *testing.T) {
2314+
builder := Dialect(dialect.YDB).
2315+
BatchUpdate("users").
2316+
Set("status", "active").
2317+
Returning("id", "status")
2318+
2319+
query, args, err := builder.QueryErr()
2320+
2321+
require.Empty(t, query)
2322+
require.Empty(t, args)
2323+
require.Error(t, err)
2324+
})
2325+
2326+
t.Run("BATCH UPDATE with UPDATE ON pattern should error", func(t *testing.T) {
23012327
d := Dialect(dialect.YDB)
2302-
query, args := d.BatchUpdate("users").
2328+
subquery := d.Select("id").From(Table("orders")).Where(EQ("status", "pending"))
2329+
2330+
builder := d.BatchUpdate("users").
23032331
Set("status", "active").
2304-
Query()
2332+
On(subquery)
23052333

2306-
require.Equal(t, "BATCH UPDATE `users` SET `status` = $p0", query)
2307-
require.Equal(t, []any{
2308-
driver.NamedValue{Name: "p0", Value: "active"},
2309-
}, args)
2334+
query, args, err := builder.QueryErr()
2335+
2336+
require.Empty(t, query)
2337+
require.Empty(t, args)
2338+
require.Error(t, err)
2339+
require.Contains(t, err.Error(), "BATCH UPDATE: UPDATE ON pattern is not supported")
23102340
})
2341+
}
23112342

2312-
t.Run("BATCH UPDATE with multiple SET clauses", func(t *testing.T) {
2343+
func TestBatchDelete_YDB(t *testing.T) {
2344+
t.Run("Basic BATCH DELETE", func(t *testing.T) {
23132345
d := Dialect(dialect.YDB)
2314-
query, args := d.BatchUpdate("products").
2315-
Set("price", 100).
2316-
Set("stock", 50).
2317-
Set("updated_at", "2024-01-01").
2318-
Where(EQ("category", "electronics")).
2346+
query, args := d.BatchDelete("my_table").
2347+
Where(And(GT("Key1", 1), GTE("Key2", "One"))).
23192348
Query()
23202349

2321-
require.Contains(t, query, "BATCH UPDATE `products` SET")
2322-
require.Contains(t, query, "`price` = $p0")
2323-
require.Contains(t, query, "`stock` = $p1")
2324-
require.Contains(t, query, "`updated_at` = $p2")
2325-
require.Contains(t, query, "WHERE `category` = $p3")
2326-
require.Len(t, args, 4)
2350+
require.Equal(t, "BATCH DELETE FROM `my_table` WHERE `Key1` > $p0 AND `Key2` >= $p1", query)
2351+
require.Equal(t, []any{
2352+
driver.NamedValue{Name: "p0", Value: 1},
2353+
driver.NamedValue{Name: "p1", Value: "One"},
2354+
}, args)
23272355
})
23282356

2329-
t.Run("BATCH UPDATE on non-YDB dialect should error", func(t *testing.T) {
2357+
t.Run("BATCH DELETE on non-YDB dialect should error", func(t *testing.T) {
23302358
builder := Dialect(dialect.MySQL).
2331-
BatchUpdate("users").
2332-
Set("status", "active").
2333-
Where(GT("created_at", "2024-01-01"))
2359+
BatchDelete("users").
2360+
Where(GT("id", 100))
23342361

23352362
query, args, err := builder.QueryErr()
23362363

@@ -2339,17 +2366,32 @@ func TestBatchUpdate_YDB(t *testing.T) {
23392366
require.Error(t, err)
23402367
})
23412368

2342-
t.Run("BATCH UPDATE with RETURNING should error", func(t *testing.T) {
2369+
t.Run("BATCH DELETE with RETURNING should error", func(t *testing.T) {
23432370
builder := Dialect(dialect.YDB).
2344-
BatchUpdate("users").
2345-
Set("status", "active").
2346-
Returning("id", "status")
2371+
BatchDelete("users").
2372+
Where(GT("id", 100)).
2373+
Returning("id")
2374+
2375+
query, args, err := builder.QueryErr()
2376+
2377+
require.Empty(t, query)
2378+
require.Empty(t, args)
2379+
require.Error(t, err)
2380+
})
2381+
2382+
t.Run("BATCH DELETE with DELETE ON pattern should error", func(t *testing.T) {
2383+
d := Dialect(dialect.YDB)
2384+
subquery := d.Select("id").From(Table("users")).Where(EQ("status", "deleted"))
2385+
2386+
builder := d.BatchDelete("users").
2387+
On(subquery)
23472388

23482389
query, args, err := builder.QueryErr()
23492390

23502391
require.Empty(t, query)
23512392
require.Empty(t, args)
23522393
require.Error(t, err)
2394+
require.Contains(t, err.Error(), "BATCH DELETE: DELETE ON pattern is not supported")
23532395
})
23542396
}
23552397

0 commit comments

Comments
 (0)