codegen: add generator for soft-deletion

This commit is contained in:
2026-09-19 21:36:31 -07:00
parent 9d4a1eab10
commit b064948d24
4 changed files with 89 additions and 1 deletions

View File

@@ -296,6 +296,7 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
updatePairs := make([]string, 0, len(tbl.Columns))
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
hasSoftDelete := tbl.HasSoftDelete()
// Assemble data for building SQL "insert" and "update" strings
for _, col := range tbl.Columns {
@@ -312,6 +313,10 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
if col.Name == "created_at" && hasCreatedAt {
continue
}
// Soft-delete state is managed by SoftDeleteXyz(), not by updates
if hasSoftDelete && (col.Name == "is_deleted" || col.Name == "deleted_at") {
continue
}
if !col.IsPrimaryKey { // Don't try to update primary key columns (mainly for w/o rowid tables)
updatePairs = append(updatePairs, col.Name+"="+val)
}
@@ -828,6 +833,66 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
return funcDecl
}
// GenerateSoftDeleteItemFunc produces an AST for the `SoftDeleteXyz()` function, which is only
// generated for tables that have both an "is_deleted" and a "deleted_at" column.
// E.g., a table with `table.TypeName = "foods"` will produce a "SoftDeleteFood()" function.
func GenerateSoftDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
whereClauses := []string{}
for _, c := range tbl.PrimaryKeyColumns() {
whereClauses = append(whereClauses, fmt.Sprintf("%s = :%s", c.Name, c.Name))
}
setPairs := []string{"is_deleted = :is_deleted", "deleted_at = :deleted_at"}
updateStmt := fmt.Sprintf("\n\t update %s\n\t set %s\n\t where %s\n\t",
tbl.TableName,
strings.Join(setPairs, ",\n\t "),
strings.Join(whereClauses, " and "),
)
funcBody := &ast.BlockStmt{
List: []ast.Stmt{
// item.IsDeleted = true
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(tbl.GetColumnByName("is_deleted").GoFieldName())}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{ast.NewIdent("true")},
},
// item.DeletedAt = TimestampNow()
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(tbl.GetColumnByName("deleted_at").GoFieldName())}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
},
// result := must.Get(db.DB.NamedExec(...))
&ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("result")},
Tok: token.DEFINE,
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"},
ast.NewIdent(tbl.VarName),
},
})},
},
MustBeRowsAffected(tbl),
},
}
return &ast.FuncDecl{
Doc: &ast.CommentGroup{List: []*ast.Comment{
{Text: fmt.Sprintf("// SoftDelete%s sets deleted-at and marks the item as deleted.", tbl.GoTypeName)},
}},
Recv: dbRecv,
Name: ast.NewIdent("SoftDelete" + tbl.GoTypeName),
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)},
}}}, Results: nil},
Body: funcBody,
}
}
// GenerateSQLFieldsConst produces an AST for the `const xyzSQLFields = ...` string.
func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
columns := make([]string, 0, len(tbl.Columns))

View File

@@ -69,9 +69,11 @@ func SampleValue(c pkgschema.Column, offset int) ast.Expr {
// UpdateTestFields returns the columns for which the test generator should assign its own
// sample values, in the MakeXyz() factory and in the create/update test: the same columns
// that GenerateSaveItemFunc's "update" branch writes to, minus foreign keys (which can't be
// given plausible values here) and the auto-managed "created_at"/"updated_at" columns.
// given plausible values here), the auto-managed "created_at"/"updated_at" columns, and the
// soft-delete columns (which SoftDeleteXyz() manages).
func UpdateTestFields(tbl pkgschema.Table) (ret []pkgschema.Column) {
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
hasSoftDelete := tbl.HasSoftDelete()
for _, c := range tbl.Columns {
if c.Name == "rowid" || c.IsPrimaryKey || c.IsForeignKey {
continue
@@ -82,6 +84,9 @@ func UpdateTestFields(tbl pkgschema.Table) (ret []pkgschema.Column) {
if c.Name == "updated_at" && hasUpdatedAt {
continue
}
if hasSoftDelete && (c.Name == "is_deleted" || c.Name == "deleted_at") {
continue
}
ret = append(ret, c)
}
return

View File

@@ -61,3 +61,18 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
}
return
}
// HasSoftDelete reports whether this table supports soft-deletion, i.e., whether it has both
// an "is_deleted" and a "deleted_at" column.
func (t Table) HasSoftDelete() bool {
var hasIsDeleted, hasDeletedAt bool
for _, c := range t.Columns {
if c.Name == "is_deleted" && c.Type == "integer" {
hasIsDeleted = true
}
if c.Name == "deleted_at" && c.Type == "integer" {
hasDeletedAt = true
}
}
return hasIsDeleted && hasDeletedAt
}