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 generator/templates/builders_delete.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,24 @@ type DeleteManyBuilder[M any] struct {
func (b *DeleteManyBuilder[M]) Exec(ctx context.Context) (int64, error) {
return b.execFunc(ctx, b.where)
}

type DeleteBuilder[M any, S any, O any] struct {
where UniquePredicate[M]
selects *S
omits *O
execFunc func(ctx context.Context, where UniquePredicate[M], selects *S, omits *O) (*M, error)
}

func (b *DeleteBuilder[M, S, O]) Select(selects S) *DeleteBuilder[M, S, O] {
b.selects = &selects
return b
}

func (b *DeleteBuilder[M, S, O]) Omit(omits O) *DeleteBuilder[M, S, O] {
b.omits = &omits
return b
}

func (b *DeleteBuilder[M, S, O]) Exec(ctx context.Context) (*M, error) {
return b.execFunc(ctx, b.where, b.selects, b.omits)
}
136 changes: 136 additions & 0 deletions generator/templates/model_delete.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,139 @@ func (d *{{ .Model.Name }}Delegate) runDeleteMany(ctx context.Context, preds []P
}
return result.RowsAffected()
}

func (d *{{ .Model.Name }}Delegate) Delete(where UniquePredicate[{{ .Model.Name }}]) *DeleteBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit] {
return &DeleteBuilder[{{ .Model.Name }}, {{ .Model.Name }}Select, {{ .Model.Name }}Omit]{
where: where,
execFunc: d.executeDelete,
}
}

func (d *{{ .Model.Name }}Delegate) executeDelete(ctx context.Context, where UniquePredicate[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) {
if len(d.extensions) == 0 {
return d.runDelete(ctx, where, selects, omits)
}

curr := func(c context.Context, w UniquePredicate[{{ .Model.Name }}], s *{{ .Model.Name }}Select, o *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) {
return d.runDelete(c, w, s, o)
}

for _, ext := range slices.Backward(d.extensions) {
if ext.Delete != nil {
next, hook := curr, ext.Delete
curr = func(c context.Context, w UniquePredicate[{{ .Model.Name }}], s *{{ .Model.Name }}Select, o *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) {
return hook(c, w, s, o, next)
}
}
}

return curr(ctx, where, selects, omits)
}

func (d *{{ .Model.Name }}Delegate) runDelete(ctx context.Context, where UniquePredicate[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error) {
if err := where.Validate(); err != nil {
return nil, err
}

returningCols := select{{ .Model.Name }}Cols(selects, omits, {{ lowercase .Model.Name }}PKCols...)

hasRelations := selects != nil && selects.hasAnyRelation()
useTx := !d.client.dialect.SupportsDeleteReturning || hasRelations

if useTx {
var res *{{ .Model.Name }}
err := d.client.transaction(ctx, func(txQ *Queries) error {
var err error
res, err = txQ.{{ .Model.Name }}.executeFindUnique(ctx, where, nil, selects, omits)
if err != nil {
return err
}
if res == nil {
return sql.ErrNoRows
}

// Build DELETE statement by PK
var deleteSb strings.Builder
deleteSb.WriteString("DELETE FROM ")
txQ.dialect.WriteQuotedIdent(&deleteSb, "{{ .Model.EffectiveTableName }}")
deleteSb.WriteString(" WHERE ")

var pkPreds []PredicateOf[{{ .Model.Name }}]
{{- if .Model.CompositePK }}
{{- range $fName := .Model.CompositePK }}
{{- $field := $.Model.GetField $fName }}
pkPreds = append(pkPreds, Predicate[{{ $.Model.Name }}]{
Data: PredicateData{
Column: "{{ $field.EffectiveColName }}",
Operator: "=",
Value: res.{{ capitalize $field.Name }},
},
})
{{- end }}
{{- else }}
{{- range $field := .Model.ScalarFields }}
{{- if $field.IsID }}
pkPreds = append(pkPreds, Predicate[{{ $.Model.Name }}]{
Data: PredicateData{
Column: "{{ $field.EffectiveColName }}",
Operator: "=",
Value: res.{{ capitalize $field.Name }},
},
})
{{- end }}
{{- end }}
{{- end }}

whereClause, vals := CompilePredicates(txQ.dialect, pkPreds)
deleteSb.WriteString(whereClause)

_, err = txQ.exec(ctx, deleteSb.String(), vals...)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return res, nil
}

// Dialect supports RETURNING, and no relations need loading: run direct DELETE ... RETURNING
var sb strings.Builder
sb.WriteString("DELETE FROM ")
d.client.dialect.WriteQuotedIdent(&sb, "{{ .Model.EffectiveTableName }}")

whereClause, vals := CompilePredicates(d.client.dialect, []PredicateOf[{{ .Model.Name }}]{where})
if whereClause != "" {
sb.WriteString(" WHERE ")
sb.WriteString(whereClause)
}

sb.WriteString(" RETURNING ")
for i, col := range returningCols {
if i > 0 {
sb.WriteString(", ")
}
d.client.dialect.WriteQuotedIdent(&sb, col)
}

rows, err := d.client.query(ctx, sb.String(), vals...)
if err != nil {
return nil, err
}
defer rows.Close()

if !rows.Next() {
if err := rows.Err(); err != nil {
return nil, err
}
return nil, sql.ErrNoRows
}

var row {{ .Model.Name }}
if err := rows.Scan(row.ScanFields(returningCols)...); err != nil {
return nil, err
}
return &row, nil
}
2 changes: 2 additions & 0 deletions generator/templates/model_structs.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type {{ .Model.Name }}FindUniqueQuery = func(ctx context.Context, where UniquePr
type {{ .Model.Name }}FindFirstQuery = func(ctx context.Context, params QueryParams[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error)
type {{ .Model.Name }}FindManyQuery = func(ctx context.Context, params QueryParams[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) ([]*{{ .Model.Name }}, error)
type {{ .Model.Name }}DeleteManyQuery = func(ctx context.Context, preds []PredicateOf[{{ .Model.Name }}]) (int64, error)
type {{ .Model.Name }}DeleteQuery = func(ctx context.Context, where UniquePredicate[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit) (*{{ .Model.Name }}, error)
type {{ .Model.Name }}CountQuery = func(ctx context.Context, params QueryParams[{{ .Model.Name }}]) (int64, error)

type {{ .Model.Name }}Extension struct {
Expand All @@ -138,6 +139,7 @@ type {{ .Model.Name }}Extension struct {
FindFirst func(ctx context.Context, params QueryParams[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit, next {{ .Model.Name }}FindFirstQuery) (*{{ .Model.Name }}, error)
FindMany func(ctx context.Context, params QueryParams[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit, next {{ .Model.Name }}FindManyQuery) ([]*{{ .Model.Name }}, error)
DeleteMany func(ctx context.Context, preds []PredicateOf[{{ .Model.Name }}], next {{ .Model.Name }}DeleteManyQuery) (int64, error)
Delete func(ctx context.Context, where UniquePredicate[{{ .Model.Name }}], selects *{{ .Model.Name }}Select, omits *{{ .Model.Name }}Omit, next {{ .Model.Name }}DeleteQuery) (*{{ .Model.Name }}, error)
Count func(ctx context.Context, params QueryParams[{{ .Model.Name }}], next {{ .Model.Name }}CountQuery) (int64, error)
}

Expand Down
147 changes: 147 additions & 0 deletions integration/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,150 @@ func TestDeleteMany_NoMatches(t *testing.T) {
t.Errorf("expected 0 deleted users, got %d", count)
}
}

func TestDeleteBasic(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()

u, err := db.User.Create().
SetEmail("todelete@example.com").
SetPhoneNum("+123456").
SetRole(valk.UserRole.Student).
Exec(ctx)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}

deleted, err := db.User.Delete(user.Email.EQ("todelete@example.com")).Exec(ctx)
if err != nil {
t.Fatalf("failed to delete user: %v", err)
}

if deleted.Email != "todelete@example.com" {
t.Errorf("expected deleted user email to be todelete@example.com, got %s", deleted.Email)
}
if deleted.Id != u.Id {
t.Errorf("expected deleted user id to be %s, got %s", u.Id, deleted.Id)
}

var count int
err = db.Raw().QueryRowContext(ctx, `SELECT COUNT(*) FROM "User" WHERE email = 'todelete@example.com'`).Scan(&count)
if err != nil {
t.Fatalf("failed to query count: %v", err)
}
if count != 0 {
t.Errorf("expected user to be completely deleted from DB, got count %d", count)
}
}

func TestDeleteSelectOmit(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()

_, err := db.User.Create().
SetEmail("selectdelete@example.com").
SetPhoneNum("+55555").
SetRole(valk.UserRole.Student).
Exec(ctx)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}

deleted, err := db.User.Delete(user.Email.EQ("selectdelete@example.com")).
Select(valk.UserSelect{Email: true}).
Exec(ctx)
if err != nil {
t.Fatalf("failed to delete: %v", err)
}

if deleted.Email != "selectdelete@example.com" {
t.Errorf("expected email to be populated, got %s", deleted.Email)
}
if deleted.PhoneNum != "" {
t.Errorf("expected phoneNum to be omitted (empty), got %s", deleted.PhoneNum)
}
}

func TestDeleteWithRelations(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()

u, err := db.User.Create().
SetEmail("reldelete@example.com").
SetPhoneNum("+777").
Exec(ctx)
if err != nil {
t.Fatalf("failed to create user: %v", err)
}

_, err = db.Profile.Create().
SetBio("My bio").
SetUserId(u.Id).
Exec(ctx)
if err != nil {
t.Fatalf("failed to create profile: %v", err)
}

deleted, err := db.User.Delete(user.Id.EQ(u.Id)).
Select(valk.UserSelect{
Email: true,
Profile: &valk.ProfileSelect{
Bio: true,
},
}).
Exec(ctx)
if err != nil {
t.Fatalf("failed to delete user: %v", err)
}

if deleted.Profile == nil {
t.Errorf("expected related profile to be loaded, got nil")
} else if deleted.Profile.Bio == nil || *deleted.Profile.Bio != "My bio" {
t.Errorf("expected loaded profile bio to be 'My bio', got %v", deleted.Profile.Bio)
}
}

func TestDeleteNotFound(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()

_, err := db.User.Delete(user.Email.EQ("notfound@example.com")).Exec(ctx)
if err == nil {
t.Fatal("expected error when deleting non-existent row, got nil")
}
}

func TestDeleteHooks(t *testing.T) {
db, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()

hookCalled := false
db.User.Use(user.Extension{
Delete: func(ctx context.Context, where valk.UniquePredicate[valk.User], selects *valk.UserSelect, omits *valk.UserOmit, next valk.UserDeleteQuery) (*valk.User, error) {
hookCalled = true
return next(ctx, where, selects, omits)
},
})

_, err := db.User.Create().
SetEmail("hookdelete@example.com").
SetPhoneNum("+999").
Exec(ctx)
if err != nil {
t.Fatalf("failed to create: %v", err)
}

_, err = db.User.Delete(user.Email.EQ("hookdelete@example.com")).Exec(ctx)
if err != nil {
t.Fatalf("failed to delete: %v", err)
}

if !hookCalled {
t.Errorf("expected delete hook to be called, but it wasn't")
}
}
Loading
Loading