From 6cf0b66dd54a8454d5dc1cec2be2a2182f7c7cb5 Mon Sep 17 00:00:00 2001 From: ~wispem-wantex Date: Sat, 19 Sep 2026 21:36:31 -0700 Subject: [PATCH] codegen: CreateUpdateDelete test no longer creates an "Update" block if there's no updateable fields --- pkg/codegen/modelgenerate/generate_model.go | 22 ++--- .../modelgenerate/generate_testfile.go | 82 +++++++++---------- pkg/schema/table.go | 18 ++++ pkg/schema/table_test.go | 34 ++++++++ 4 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 pkg/schema/table_test.go diff --git a/pkg/codegen/modelgenerate/generate_model.go b/pkg/codegen/modelgenerate/generate_model.go index dcc0eed..cdd986a 100644 --- a/pkg/codegen/modelgenerate/generate_model.go +++ b/pkg/codegen/modelgenerate/generate_model.go @@ -296,9 +296,8 @@ 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 + // Assemble SQL "insert" string for _, col := range tbl.Columns { if col.Name == "rowid" { continue @@ -309,23 +308,20 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl { val = fmt.Sprintf("nullif(%s, 0)", val) } insertVals = append(insertVals, val) - // created_at should not be updated after creation - 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) - } } insertStmt := fmt.Sprintf("\n\t\t insert into %s (%s)\n\t\t values (%s)\n\t\t", tbl.TableName, strings.Join(insertCols, ", "), strings.Join(insertVals, ", "), ) + // ...and "update" string + for _, col := range tbl.GetUpdateableColumns() { + val := ":" + col.Name + if col.IsNullableForeignKey() { + val = fmt.Sprintf("nullif(%s, 0)", val) + } + updatePairs = append(updatePairs, col.Name+"="+val) + } updateStmt := fmt.Sprintf("\n\t\t update %s\n\t\t set %s\n\t\t where rowid = :rowid\n\t\t", tbl.TableName, strings.Join(updatePairs, ",\n\t\t "), diff --git a/pkg/codegen/modelgenerate/generate_testfile.go b/pkg/codegen/modelgenerate/generate_testfile.go index 904897f..0dae4d6 100644 --- a/pkg/codegen/modelgenerate/generate_testfile.go +++ b/pkg/codegen/modelgenerate/generate_testfile.go @@ -67,26 +67,18 @@ 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), the auto-managed "created_at"/"updated_at" columns, and the -// soft-delete columns (which SoftDeleteXyz() manages). +// sample values in MakeXyz() and the create/update test. It starts with Table.GetUpdateableColumns() +// and removes fields the generated test should not assign directly: foreign keys and auto-managed +// updated_at. 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 - } - if c.Name == "created_at" && hasCreatedAt { + _, hasUpdatedAt := tbl.HasAutoTimestamps() + for _, c := range tbl.GetUpdateableColumns() { + if c.IsForeignKey { continue } if c.Name == "updated_at" && hasUpdatedAt { continue } - if hasSoftDelete && (c.Name == "is_deleted" || c.Name == "deleted_at") { - continue - } ret = append(ret, c) } return @@ -486,39 +478,41 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam makeDeepEqual(testObj, testObj2), ) - stmts = append(stmts, - BlankLine(), - Comment("Update"), - ) + if len(tbl.GetUpdateableColumns()) > 0 { // Skip if no mutable fields + stmts = append(stmts, + BlankLine(), + Comment("Update"), + ) - // item.Description = - // ...one assignment per updatable field - for _, c := range updateTestFields { - stmts = append(stmts, &ast.AssignStmt{ - Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent(c.GoFieldName())}}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{SampleValue(c, 2)}, - }) + // item.Description = + // ...one assignment per updatable field + for _, c := range updateTestFields { + stmts = append(stmts, &ast.AssignStmt{ + Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent(c.GoFieldName())}}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{SampleValue(c, 2)}, + }) + } + + stmts = append(stmts, + // TestDB.SaveItem(&item) + &ast.ExprStmt{X: &ast.CallExpr{ + Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent("Save" + tbl.GoTypeName)}, + Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}}, + }}, + + // item2 = must.Get(TestDB.GetItemByID(item.ID)) + &ast.AssignStmt{ + Lhs: []ast.Expr{testObj2}, + Tok: token.ASSIGN, + Rhs: []ast.Expr{mustCall(getItemByPKCall(testObj))}, + }, + + // if deep.Equal(...) {...} + makeDeepEqual(testObj, testObj2), + ) } - stmts = append(stmts, - // TestDB.SaveItem(&item) - &ast.ExprStmt{X: &ast.CallExpr{ - Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent("Save" + tbl.GoTypeName)}, - Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}}, - }}, - - // item2 = must.Get(TestDB.GetItemByID(item.ID)) - &ast.AssignStmt{ - Lhs: []ast.Expr{testObj2}, - Tok: token.ASSIGN, - Rhs: []ast.Expr{mustCall(getItemByPKCall(testObj))}, - }, - - // if deep.Equal(...) {...} - makeDeepEqual(testObj, testObj2), - ) - indexGets, hasIndexedGets := []ast.Stmt{ BlankLine(), Comment("Indexed lookups"), diff --git a/pkg/schema/table.go b/pkg/schema/table.go index 965ba78..4440395 100644 --- a/pkg/schema/table.go +++ b/pkg/schema/table.go @@ -76,3 +76,21 @@ func (t Table) HasSoftDelete() bool { } return hasIsDeleted && hasDeletedAt } + +// GetUpdateableColumns returns the columns that should be written to by an "update" operation. +func (t Table) GetUpdateableColumns() (ret []Column) { + hasCreatedAt, _ := t.HasAutoTimestamps() + for _, col := range t.Columns { + if col.Name == "rowid" || col.IsPrimaryKey { + continue // Rowid / primary key is never updateable + } + if col.Name == "created_at" && hasCreatedAt { + continue // Update shouldn't write `created_at` + } + if t.HasSoftDelete() && (col.Name == "is_deleted" || col.Name == "deleted_at") { + continue // Soft-delete state is managed by SoftDeleteXyz(), not by updates + } + ret = append(ret, col) + } + return +} diff --git a/pkg/schema/table_test.go b/pkg/schema/table_test.go new file mode 100644 index 0000000..6c3ffc8 --- /dev/null +++ b/pkg/schema/table_test.go @@ -0,0 +1,34 @@ +package schema + +import "testing" + +func TestTableGetUpdateableColumns(t *testing.T) { + tbl := Table{ + Columns: []Column{ + {Name: "rowid"}, + {Name: "version", Type: "integer", IsPrimaryKey: true}, + {Name: "created_at", Type: "integer"}, + {Name: "updated_at", Type: "integer"}, + {Name: "is_deleted", Type: "integer"}, + {Name: "deleted_at", Type: "integer"}, + {Name: "owner_id", Type: "integer", IsForeignKey: true}, + {Name: "name", Type: "text"}, + }, + } + + gotColumns := tbl.GetUpdateableColumns() + got := make([]string, 0, len(gotColumns)) + for _, col := range gotColumns { + got = append(got, col.Name) + } + + want := []string{"updated_at", "owner_id", "name"} + if len(got) != len(want) { + t.Fatalf("GetUpdateableColumns() = %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("GetUpdateableColumns() = %v, want %v", got, want) + } + } +}