From b064948d24c8a7487f61ffcc5020146536e978f5 Mon Sep 17 00:00:00 2001 From: ~wispem-wantex Date: Sat, 19 Sep 2026 21:36:31 -0700 Subject: [PATCH] codegen: add generator for soft-deletion --- cmd/subcmd_generate_models.go | 3 + pkg/codegen/modelgenerate/generate_model.go | 65 +++++++++++++++++++ .../modelgenerate/generate_testfile.go | 7 +- pkg/schema/table.go | 15 +++++ 4 files changed, 89 insertions(+), 1 deletion(-) diff --git a/cmd/subcmd_generate_models.go b/cmd/subcmd_generate_models.go index b1ced39..2d6ddc0 100644 --- a/cmd/subcmd_generate_models.go +++ b/cmd/subcmd_generate_models.go @@ -69,6 +69,9 @@ var generate_model = &cobra.Command{ modelgenerate.GenerateSaveItemFunc(table), modelgenerate.GenerateDeleteItemFunc(table), ) + if table.HasSoftDelete() { + decls = append(decls, modelgenerate.GenerateSoftDeleteItemFunc(table)) + } if table.IsWithoutRowid { decls = append(decls, modelgenerate.GenerateGetItemBy(table, table.PrimaryKeyColumns()), diff --git a/pkg/codegen/modelgenerate/generate_model.go b/pkg/codegen/modelgenerate/generate_model.go index 88e7e2e..dcc0eed 100644 --- a/pkg/codegen/modelgenerate/generate_model.go +++ b/pkg/codegen/modelgenerate/generate_model.go @@ -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)) diff --git a/pkg/codegen/modelgenerate/generate_testfile.go b/pkg/codegen/modelgenerate/generate_testfile.go index 8be63e4..703a518 100644 --- a/pkg/codegen/modelgenerate/generate_testfile.go +++ b/pkg/codegen/modelgenerate/generate_testfile.go @@ -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 diff --git a/pkg/schema/table.go b/pkg/schema/table.go index 88c9722..965ba78 100644 --- a/pkg/schema/table.go +++ b/pkg/schema/table.go @@ -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 +}