Skip to content

Commit b1e252a

Browse files
authored
feat: support bulk delete (#25)
1 parent f9711da commit b1e252a

File tree

6 files changed

+95
-2
lines changed

6 files changed

+95
-2
lines changed

db.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,10 @@ func (db *DB) Delete(ctx context.Context, record interface{}) error {
164164
return mustDelete(ctx, db.Exec, record)
165165
}
166166

167+
func (db *DB) BulkDelete(ctx context.Context, records interface{}) error {
168+
return mustBulkDelete(ctx, db.Exec, records)
169+
}
170+
167171
// Start a DB transaction. It returns an interface w/ most of the methods DB provides.
168172
func (db *DB) Begin(ctx context.Context, readOnly bool) (*Tx, error) {
169173
client, err := db.Client.BeginTx(ctx, &stdsql.TxOptions{

delete.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
stdsql "database/sql"
66
"errors"
77
"fmt"
8+
"reflect"
89

910
"github.com/azer/crud/v2/meta"
1011
"github.com/azer/crud/v2/sql"
1112
)
1213

13-
func deleteRow(ctx context.Context, exec ExecFn, record interface{}) (stdsql.Result, error) {
14+
func deleteRow(ctx context.Context, exec ExecFn, record any) (stdsql.Result, error) {
1415
table, err := NewTable(record)
1516

1617
if err != nil {
@@ -25,7 +26,31 @@ func deleteRow(ctx context.Context, exec ExecFn, record interface{}) (stdsql.Res
2526
return exec(ctx, sql.DeleteQuery(table.SQLName, pk.SQL.Name), meta.StructFieldValue(record, pk.Name))
2627
}
2728

28-
func mustDelete(ctx context.Context, exec ExecFn, record interface{}) error {
29+
func deleteRows(ctx context.Context, exec ExecFn, records []any) (stdsql.Result, error) {
30+
if len(records) == 0 {
31+
return nil, errors.New("no records to delete")
32+
}
33+
34+
table, err := NewTable(records[0])
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
pk := table.PrimaryKeyField()
40+
if pk == nil {
41+
return nil, fmt.Errorf("Table '%s' (%s) doesn't have a primary-key field", table.Name, table.SQLName)
42+
}
43+
44+
pkValues := make([]any, 0, len(records))
45+
for _, record := range records {
46+
pkValue := meta.StructFieldValue(record, pk.Name)
47+
pkValues = append(pkValues, pkValue)
48+
}
49+
50+
return exec(ctx, sql.BulkDeleteQuery(table.SQLName, pk.SQL.Name, len(records)), pkValues...)
51+
}
52+
53+
func mustDelete(ctx context.Context, exec ExecFn, record any) error {
2954
result, err := deleteRow(ctx, exec, record)
3055
if err != nil {
3156
return err
@@ -42,3 +67,35 @@ func mustDelete(ctx context.Context, exec ExecFn, record interface{}) error {
4267

4368
return nil
4469
}
70+
71+
func mustBulkDelete(ctx context.Context, exec ExecFn, value any) error {
72+
v := reflect.ValueOf(value)
73+
if v.Kind() != reflect.Slice {
74+
return errors.New("records must be a slice")
75+
}
76+
77+
records := make([]any, 0, v.Len())
78+
for i := 0; i < v.Len(); i++ {
79+
records = append(records, v.Index(i).Interface())
80+
}
81+
82+
if len(records) == 0 {
83+
return errors.New("no records to delete")
84+
}
85+
86+
result, err := deleteRows(ctx, exec, records)
87+
if err != nil {
88+
return err
89+
}
90+
91+
count, err := result.RowsAffected()
92+
if err != nil {
93+
return err
94+
}
95+
96+
if count == 0 {
97+
return stdsql.ErrNoRows
98+
}
99+
100+
return nil
101+
}

delete_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,19 @@ func TestMustDeleteNotMatching(t *testing.T) {
5555
Name: "Yolo",
5656
}))
5757
}
58+
59+
func TestBulkDelete(t *testing.T) {
60+
ctx := context.Background()
61+
62+
require.NoError(t, CreateUserProfiles(ctx))
63+
64+
nova := UserProfile{}
65+
err := DB.Read(ctx, &nova, "SELECT * FROM user_profiles WHERE name = 'Nova'")
66+
assert.Nil(t, err)
67+
68+
assert.Nil(t, DB.BulkDelete(ctx, []UserProfile{nova}))
69+
70+
novac := UserProfile{}
71+
err = DB.Read(ctx, &novac, "SELECT * FROM user_profile WHERE name = 'Nova'")
72+
assert.NotNil(t, err)
73+
}

sql/table.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ func DeleteQuery(tableName, index string) string {
152152
return fmt.Sprintf("DELETE FROM `%s` WHERE `%s`=?", tableName, index)
153153
}
154154

155+
func BulkDeleteQuery(tableName, index string, numRecords int) string {
156+
pattern := fmt.Sprintf("(%s)", repeatComma(1, "?"))
157+
questionMarks := slices.Repeat([]string{pattern}, numRecords)
158+
159+
return fmt.Sprintf("DELETE FROM `%s` WHERE `%s` IN (%s)",
160+
tableName, index, strings.Join(questionMarks, ","))
161+
}
162+
155163
func quoteColumnNames(columns []string) []string {
156164
var cols []string
157165
for _, c := range columns {

sql/table_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,7 @@ func TestUpdateQuery(t *testing.T) {
101101
func TestDeleteQuery(t *testing.T) {
102102
assert.Equal(t, sql.DeleteQuery("yolo", "id"), "DELETE FROM `yolo` WHERE `id`=?")
103103
}
104+
105+
func TestBulkDeleteQuery(t *testing.T) {
106+
assert.Equal(t, sql.BulkDeleteQuery("yolo", "id", 3), "DELETE FROM `yolo` WHERE `id` IN ((?),(?),(?))")
107+
}

transaction.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ func (tx *Tx) BulkCreate(ctx context.Context, records any) error {
5757
return bulkCreate(ctx, tx.Exec, records)
5858
}
5959

60+
func (tx *Tx) BulkDelete(ctx context.Context, records any) error {
61+
return mustBulkDelete(ctx, tx.Exec, records)
62+
}
63+
6064
// Replace given record to the database.
6165
func (tx *Tx) Replace(ctx context.Context, record interface{}) error {
6266
return replace(ctx, tx.Exec, record)

0 commit comments

Comments
 (0)