Compare commits

..

3 Commits

Author SHA1 Message Date
04676461ff doc: add TODOs
All checks were successful
CI / build-docker (push) Successful in 5s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Successful in 26s
2026-02-14 22:56:43 -08:00
a36058fdbe codegen (refactor): clean up helpers a bit 2026-02-14 22:54:22 -08:00
f0c152cfe4 codegen: add auto-timestamp checking to generated tests 2026-02-14 22:17:59 -08:00
6 changed files with 218 additions and 185 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ sample_data/data
# Legacy versions # Legacy versions
.cmd-old/ .cmd-old/
pkg/.testapp pkg/.testapp
.claude

View File

@ -26,6 +26,14 @@ TODO: language-server-for-TODO.txt
TODO: auto-migration-checker TODO: auto-migration-checker
- Use `pkg/schema` to test whether a base schema plus a migration equals a new schema - Use `pkg/schema` to test whether a base schema plus a migration equals a new schema
TODO: codegen SaveXyz update path doesn't check foreign keys
- Insert path has FK error handling, but the update path wraps everything in Must
- An update that violates an FK constraint will panic instead of returning an error
TODO: codegen `without rowid` tables properly TODO: codegen `without rowid` tables properly
TODO: generated test file inclues global test DB setup, which is wrong TODO: generated test file inclues global test DB setup, which is wrong
TODO: join-tables
- handle codegen for without rowid tables properly
- add "get all "

View File

@ -7,6 +7,7 @@ sudo docker run --rm -it \
-v "$(go env GOMODCACHE):/gocache-vol/mod-cache" \ -v "$(go env GOMODCACHE):/gocache-vol/mod-cache" \
-e GOMODCACHE=/gocache-vol/mod-cache \ -e GOMODCACHE=/gocache-vol/mod-cache \
-e GOLANGCI_LINT_CACHE=/gocache-vol/lint-cache \ -e GOLANGCI_LINT_CACHE=/gocache-vol/lint-cache \
-v /memory:/memory \
--workdir /code \ --workdir /code \
--net host \ --net host \
gas gas

View File

@ -0,0 +1,11 @@
package modelgenerate
import "go/ast"
// mustCall wraps a call expression in Must(...), producing AST for Must(inner).
func mustCall(inner ast.Expr) *ast.CallExpr {
return &ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{inner},
}
}

View File

@ -13,12 +13,18 @@ import (
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils" "git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
) )
func GenerateIDType(table schema.Table) *ast.GenDecl { // ---------------
// e.g., `type FoodID int` // Helpers
return &ast.GenDecl{ // ---------------
Tok: token.TYPE,
Specs: []ast.Spec{&ast.TypeSpec{Name: ast.NewIdent(table.TypeIDName), Type: ast.NewIdent("int")}}, var (
} dbRecv = &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
dbDB = &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("DB")}
fmtErrorf = &ast.SelectorExpr{X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Errorf")}
)
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
} }
func fkFieldName(col schema.Column) string { func fkFieldName(col schema.Column) string {
@ -29,6 +35,19 @@ func fkFieldName(col schema.Column) string {
} }
} }
// ---------------
// Generators
// ---------------
// GenerateIDType produces an AST for the model's ID field.
func GenerateIDType(table schema.Table) *ast.GenDecl {
// e.g., `type FoodID int`
return &ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{&ast.TypeSpec{Name: ast.NewIdent(table.TypeIDName), Type: ast.NewIdent("int")}},
}
}
// GenerateModelAST produces an AST for a struct type corresponding to the model. // GenerateModelAST produces an AST for a struct type corresponding to the model.
// TODO: generate the right field types here based on column types. // TODO: generate the right field types here based on column types.
func GenerateModelAST(table schema.Table) *ast.GenDecl { func GenerateModelAST(table schema.Table) *ast.GenDecl {
@ -203,7 +222,7 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
Tok: token.ASSIGN, Tok: token.ASSIGN,
Rhs: []ast.Expr{ Rhs: []ast.Expr{
&ast.CallExpr{ &ast.CallExpr{
Fun: &ast.SelectorExpr{X: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("DB")}, Sel: ast.NewIdent("Get")}, Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
Args: []ast.Expr{ Args: []ast.Expr{
&ast.CallExpr{Fun: ast.NewIdent("new"), Args: []ast.Expr{ast.NewIdent("int")}}, &ast.CallExpr{Fun: ast.NewIdent("new"), Args: []ast.Expr{ast.NewIdent("int")}},
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`select 1 from %s where rowid = ?`", col.ForeignKeyTargetTable)}, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`select 1 from %s where rowid = ?`", col.ForeignKeyTargetTable)},
@ -269,17 +288,17 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
// Do create // Do create
List: append( List: append(
func() []ast.Stmt { func() []ast.Stmt {
ret := []ast.Stmt{} ret1 := []ast.Stmt{}
if hasCreatedAt { if hasCreatedAt {
// Auto-timestamps: created_at // Auto-timestamps: created_at
ret = append(ret, &ast.AssignStmt{ ret1 = append(ret1, &ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}}, Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
Tok: token.ASSIGN, Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}}, Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
}) })
} }
namedExecStmt := &ast.CallExpr{ namedExecStmt := &ast.CallExpr{
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")}, Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
Args: []ast.Expr{ Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"}, &ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
ast.NewIdent(tbl.VarName), ast.NewIdent(tbl.VarName),
@ -287,17 +306,14 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
} }
if !hasFks { if !hasFks {
// No foreign key checking needed; just use `Must` for brevity // No foreign key checking needed; just use `Must` for brevity
return append(ret, &ast.AssignStmt{ return append(ret1, &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("result")}, Lhs: []ast.Expr{ast.NewIdent("result")},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(namedExecStmt)},
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{namedExecStmt},
}},
}) })
} }
return append(ret, return append(ret1,
// result, err := db.DB.NamedExec(`...`, u) // result, err := db.DB.NamedExec(`...`, u)
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{ Lhs: []ast.Expr{
@ -357,13 +373,10 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
Tok: token.ASSIGN, Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent(tbl.TypeIDName), Fun: ast.NewIdent(tbl.TypeIDName),
Args: []ast.Expr{&ast.CallExpr{ Args: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")}, Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")},
Args: []ast.Expr{}, Args: []ast.Expr{},
}}, })},
}},
}}, }},
}, },
), ),
@ -374,28 +387,22 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("result")}, Lhs: []ast.Expr{ast.NewIdent("result")},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"), Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
Args: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")},
Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"}, ast.NewIdent(tbl.VarName)}, Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"}, ast.NewIdent(tbl.VarName)},
}}, })},
}},
}, },
&ast.IfStmt{ &ast.IfStmt{
Cond: &ast.BinaryExpr{ Cond: &ast.BinaryExpr{
X: &ast.CallExpr{ X: mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
Args: []ast.Expr{}, Args: []ast.Expr{},
}}, }),
},
Op: token.NEQ, Op: token.NEQ,
Y: &ast.BasicLit{Kind: token.INT, Value: "1"}, Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
}, },
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("fmt.Errorf"), Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"got %s with ID (%%d), so attempted update, but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}}, Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{&ast.CallExpr{Fun: fmtErrorf, Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"got %s with ID (%%d), so attempted update, but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}}}}}}}},
}, },
}, },
}, },
@ -409,7 +416,7 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
} }
funcDecl := &ast.FuncDecl{ funcDecl := &ast.FuncDecl{
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}, Recv: dbRecv,
Name: ast.NewIdent("Save" + tbl.GoTypeName), Name: ast.NewIdent("Save" + tbl.GoTypeName),
Type: &ast.FuncType{ Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)}}}}, Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)}}}},
@ -432,7 +439,6 @@ func getByIDFuncName(tblname string) string {
// GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function. // GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function.
// E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function. // E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function.
func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl { func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}} arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}}
result := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)}, {Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")}}} result := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)}, {Names: []*ast.Ident{ast.NewIdent("err")}, Type: ast.NewIdent("error")}}}
@ -452,7 +458,7 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("err")}, Lhs: []ast.Expr{ast.NewIdent("err")},
Tok: token.ASSIGN, Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Get")}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, selectExpr, ast.NewIdent("id")}}}, Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, selectExpr, ast.NewIdent("id")}}},
}, },
&ast.IfStmt{ &ast.IfStmt{
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")}, Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}}}, Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")}, Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}}},
@ -463,7 +469,7 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
} }
funcDecl := &ast.FuncDecl{ funcDecl := &ast.FuncDecl{
Recv: recv, Recv: dbRecv,
Name: ast.NewIdent(getByIDFuncName(tbl.TableName)), Name: ast.NewIdent(getByIDFuncName(tbl.TableName)),
Type: &ast.FuncType{Params: arg, Results: result}, Type: &ast.FuncType{Params: arg, Results: result},
Body: funcBody, Body: funcBody,
@ -475,9 +481,6 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
// E.g., a table with `table.TypeName = "foods"` will produce a "GetAllFoods()" function. // E.g., a table with `table.TypeName = "foods"` will produce a "GetAllFoods()" function.
func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl { func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
funcName := "GetAll" + inflection.Plural(tbl.GoTypeName) funcName := "GetAll" + inflection.Plural(tbl.GoTypeName)
recv := &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")},
}}
result := &ast.FieldList{List: []*ast.Field{ result := &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.GoTypeName)}}, {Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.GoTypeName)}},
}} }}
@ -487,7 +490,7 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
Args: []ast.Expr{ Args: []ast.Expr{
&ast.CallExpr{ &ast.CallExpr{
Fun: &ast.SelectorExpr{ Fun: &ast.SelectorExpr{
X: ast.NewIdent("db.DB"), X: dbDB,
Sel: ast.NewIdent("Select"), Sel: ast.NewIdent("Select"),
}, },
Args: []ast.Expr{ Args: []ast.Expr{
@ -514,7 +517,7 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
} }
return &ast.FuncDecl{ return &ast.FuncDecl{
Recv: recv, Recv: dbRecv,
Name: ast.NewIdent(funcName), Name: ast.NewIdent(funcName),
Type: &ast.FuncType{ Type: &ast.FuncType{
Params: &ast.FieldList{}, Params: &ast.FieldList{},
@ -528,7 +531,6 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
// E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function. // E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function.
func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl { func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
funcName := "Delete" + tbl.GoTypeName funcName := "Delete" + tbl.GoTypeName
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.GoTypeName)}}} arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.GoTypeName)}}}
funcBody := &ast.BlockStmt{ funcBody := &ast.BlockStmt{
@ -536,25 +538,19 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("result")}, Lhs: []ast.Expr{ast.NewIdent("result")},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"), Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Exec")},
Args: []ast.Expr{&ast.CallExpr{
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Exec")},
Args: []ast.Expr{ Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"}, &ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
}, },
}}, })},
}},
}, },
&ast.IfStmt{ &ast.IfStmt{
Cond: &ast.BinaryExpr{ Cond: &ast.BinaryExpr{
X: &ast.CallExpr{ X: mustCall(
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}}, &ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}},
}, ),
},
Op: token.NEQ, Op: token.NEQ,
Y: &ast.BasicLit{Kind: token.INT, Value: "1"}, Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
}, },
@ -562,7 +558,7 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
&ast.ExprStmt{X: &ast.CallExpr{ &ast.ExprStmt{X: &ast.CallExpr{
Fun: ast.NewIdent("panic"), Fun: ast.NewIdent("panic"),
Args: []ast.Expr{&ast.CallExpr{ Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("fmt.Errorf"), Fun: fmtErrorf,
Args: []ast.Expr{ Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))}, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))},
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}, &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
@ -575,7 +571,7 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
} }
funcDecl := &ast.FuncDecl{ funcDecl := &ast.FuncDecl{
Recv: recv, Recv: dbRecv,
Name: ast.NewIdent(funcName), Name: ast.NewIdent(funcName),
Type: &ast.FuncType{Params: arg, Results: nil}, Type: &ast.FuncType{Params: arg, Results: nil},
Body: funcBody, Body: funcBody,
@ -606,11 +602,3 @@ func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
}, },
} }
} }
// ---------------
// Helpers
// ---------------
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
}

View File

@ -61,9 +61,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("db")}, Lhs: []ast.Expr{ast.NewIdent("db")},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("Create"), Fun: ast.NewIdent("Create"),
Args: []ast.Expr{&ast.CallExpr{ Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("fmt.Sprintf"), Fun: ast.NewIdent("fmt.Sprintf"),
@ -72,8 +70,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
ast.NewIdent("dbName"), ast.NewIdent("dbName"),
}, },
}}, }},
}}, })},
}},
}, },
// return db // return db
&ast.ReturnStmt{ &ast.ReturnStmt{
@ -113,6 +110,8 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
description1 := `"an item"` description1 := `"an item"`
description2 := `"a big item"` description2 := `"a big item"`
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
testCreateUpdateDelete := &ast.FuncDecl{ testCreateUpdateDelete := &ast.FuncDecl{
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName), Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
Type: &ast.FuncType{ Type: &ast.FuncType{
@ -124,7 +123,15 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
}, },
}, },
Body: &ast.BlockStmt{ Body: &ast.BlockStmt{
List: []ast.Stmt{ List: func() []ast.Stmt {
assertNotZero := func(obj *ast.Ident, field string) *ast.ExprStmt {
return &ast.ExprStmt{X: &ast.CallExpr{
Fun: ast.NewIdent("assert.NotZero"),
Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: obj, Sel: ast.NewIdent(field)}},
}}
}
stmts := []ast.Stmt{
// item := Item{Description: "an item"} // item := Item{Description: "an item"}
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{testObj}, Lhs: []ast.Expr{testObj},
@ -151,18 +158,25 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Fun: ast.NewIdent("require.NotZero"), Fun: ast.NewIdent("require.NotZero"),
Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}}, Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}}, }},
}
// After create: assert timestamps are set
if hasCreatedAt {
stmts = append(stmts, assertNotZero(testObj, "CreatedAt"))
}
if hasUpdatedAt {
stmts = append(stmts, assertNotZero(testObj, "UpdatedAt"))
}
stmts = append(stmts,
// item2 := Must(TestDB.GetItemByID(item.ID)) // item2 := Must(TestDB.GetItemByID(item.ID))
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{testObj2}, Lhs: []ast.Expr{testObj2},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"), Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}}, Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}}, })},
}},
}, },
// assert.Equal(t, "an item", item2.Description) // assert.Equal(t, "an item", item2.Description)
@ -187,18 +201,25 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName), Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName),
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
}}, }},
)
// After update: assert timestamps are still set
if hasCreatedAt {
stmts = append(stmts, assertNotZero(testObj, "CreatedAt"))
}
if hasUpdatedAt {
stmts = append(stmts, assertNotZero(testObj, "UpdatedAt"))
}
stmts = append(stmts,
// item2 = Must(TestDB.GetItemByID(item.ID)) // item2 = Must(TestDB.GetItemByID(item.ID))
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{testObj2}, Lhs: []ast.Expr{testObj2},
Tok: token.ASSIGN, Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{ Rhs: []ast.Expr{mustCall(&ast.CallExpr{
Fun: ast.NewIdent("Must"),
Args: []ast.Expr{&ast.CallExpr{
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"), Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}}, Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
}}, })},
}},
}, },
// assert.Equal(t, item.Description, item2.Description) // assert.Equal(t, item.Description, item2.Description)
@ -236,7 +257,10 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
ast.NewIdent("ErrNotInDB"), ast.NewIdent("ErrNotInDB"),
}, },
}}, }},
}, )
return stmts
}(),
}, },
} }