Compare commits
10 Commits
01e37a0e07
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 616304c7dd | |||
| 17fc8a68f6 | |||
| d572745613 | |||
| 9bfb31798c | |||
| eafeb658bd | |||
| dbf14e23b6 | |||
| ad1782c73d | |||
| ccd7e32cbf | |||
| ed4ade1956 | |||
| 9a11f3986c |
@@ -1,6 +1,6 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
on: [push, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
# These steps build the `gas` docker image.
|
||||
|
||||
@@ -22,7 +22,6 @@ func main() {
|
||||
root_cmd.AddCommand(sqlite_lint)
|
||||
root_cmd.AddCommand(cmd_init)
|
||||
root_cmd.AddCommand(generate_model)
|
||||
root_cmd.AddCommand(generate_codetable_type)
|
||||
if err := root_cmd.Execute(); err != nil {
|
||||
fmt.Println(RED + err.Error() + RESET)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"os"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/codegen/modelgenerate"
|
||||
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var generate_codetable_type = &cobra.Command{
|
||||
Use: "generate_codetable <table_name>",
|
||||
Short: "Generate a code-table enum type",
|
||||
|
||||
Args: cobra.ExactArgs(1),
|
||||
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
path := Must(cmd.Flags().GetString("schema"))
|
||||
sql, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading path %s: %w", path, err)
|
||||
}
|
||||
db := schema.InitDB(string(sql))
|
||||
schema := schema.SchemaFromDB(db)
|
||||
table, isOk := schema.Tables[args[0]]
|
||||
if !isOk {
|
||||
return ErrNoSuchTable
|
||||
}
|
||||
|
||||
vals := table.GetCodeTableValues(db)
|
||||
decls := []ast.Decl{
|
||||
modelgenerate.GenerateCodetableType(table),
|
||||
modelgenerate.GenerateCodetableEnum(table, vals),
|
||||
modelgenerate.GenerateCodetableStringerFunc(table, vals),
|
||||
}
|
||||
|
||||
file := &ast.File{
|
||||
Name: ast.NewIdent("db"), // TODO: parameterize
|
||||
Decls: decls,
|
||||
}
|
||||
|
||||
PanicIf(modelgenerate.FprintWithComments(os.Stdout, file))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
// DUPE: generate-flags
|
||||
func init() {
|
||||
generate_codetable_type.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||
generate_codetable_type.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||
generate_codetable_type.Flags().Bool("test", false, "Generate test file instead of regular file")
|
||||
}
|
||||
@@ -101,7 +101,6 @@ var generate_model = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// DUPE: generate-flags
|
||||
func init() {
|
||||
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
package modelgenerate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||
)
|
||||
|
||||
func GenerateCodetableType(table schema.Table) *ast.GenDecl {
|
||||
return &ast.GenDecl{
|
||||
Tok: token.TYPE,
|
||||
Specs: []ast.Spec{&ast.TypeSpec{Name: &ast.Ident{Name: table.GoTypeName}, Type: &ast.Ident{Name: "int"}}},
|
||||
}
|
||||
}
|
||||
|
||||
func GenerateCodetableEnum(table schema.Table, vals []string) *ast.GenDecl {
|
||||
getConstName := func(s string) string {
|
||||
return table.GoTypeName + textutils.KebabToPascal(s)
|
||||
}
|
||||
|
||||
constSpecs := []ast.Spec{}
|
||||
for i, val := range vals {
|
||||
spec := &ast.ValueSpec{
|
||||
Names: []*ast.Ident{{Name: getConstName(val)}},
|
||||
}
|
||||
// Only the first one needs `iota`
|
||||
if i == 0 {
|
||||
spec.Type = &ast.Ident{Name: table.GoTypeName}
|
||||
spec.Values = []ast.Expr{&ast.BinaryExpr{
|
||||
X: &ast.Ident{Name: "iota"},
|
||||
Op: token.ADD,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||
}}
|
||||
}
|
||||
constSpecs = append(constSpecs, spec)
|
||||
}
|
||||
|
||||
return &ast.GenDecl{Tok: token.CONST, Specs: constSpecs}
|
||||
}
|
||||
|
||||
// GenerateCodetableStringerFunc implements the `Stringer` interface by defining a `String() string` function.
|
||||
func GenerateCodetableStringerFunc(table schema.Table, vals []string) *ast.FuncDecl {
|
||||
objIdent := ast.NewIdent(table.VarName)
|
||||
return &ast.FuncDecl{
|
||||
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{objIdent}, Type: &ast.Ident{Name: table.GoTypeName}}}},
|
||||
Name: &ast.Ident{Name: "String"},
|
||||
Type: &ast.FuncType{Params: &ast.FieldList{}, Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "string"}}}}},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
// names := []string{ ... }
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.Ident{Name: "names"}},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CompositeLit{
|
||||
Type: &ast.ArrayType{Elt: &ast.Ident{Name: "string"}},
|
||||
Elts: func() []ast.Expr {
|
||||
ret := []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: `""`},
|
||||
}
|
||||
for _, val := range vals {
|
||||
ret = append(ret, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", val)})
|
||||
}
|
||||
return ret
|
||||
}(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// if int(c) < 1 || int(c) >= len(names) { return "invalid" }
|
||||
&ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: &ast.BinaryExpr{
|
||||
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
||||
Op: token.LSS,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||
},
|
||||
Op: token.LOR,
|
||||
Y: &ast.BinaryExpr{
|
||||
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
||||
Op: token.GEQ,
|
||||
Y: &ast.CallExpr{Fun: &ast.Ident{Name: "len"}, Args: []ast.Expr{&ast.Ident{Name: "names"}}},
|
||||
},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{Results: []ast.Expr{
|
||||
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Sprintf")}, Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: `"<%d=invalid>"`},
|
||||
objIdent,
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
},
|
||||
// return names[c]
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{
|
||||
&ast.IndexExpr{
|
||||
X: &ast.Ident{Name: "names"},
|
||||
Index: objIdent,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -161,9 +161,21 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||
structFieldName := col.GoFieldName()
|
||||
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
||||
|
||||
ret = append(ret, func() ast.Stmt {
|
||||
// Wrap nullable FKs in "if a.val != 0 { ... }"
|
||||
wrap := func(input ast.Stmt) ast.Stmt {
|
||||
if col.IsNullableForeignKey() {
|
||||
return &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{X: structField, Op: token.NEQ, Y: &ast.BasicLit{Kind: token.INT, Value: "0"}},
|
||||
Body: &ast.BlockStmt{List: []ast.Stmt{input}},
|
||||
}
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
if col.IsNonCodeTableForeignKey() {
|
||||
// Real foreign key; look up referent by ID to see if it exists
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
return wrap(&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||
Tok: token.DEFINE,
|
||||
@@ -197,10 +209,10 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||
})
|
||||
} else {
|
||||
// Code table value. Query the table to see if it exists
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
return wrap(&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: token.ASSIGN,
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
|
||||
@@ -234,6 +246,7 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||
},
|
||||
})
|
||||
}
|
||||
}())
|
||||
}
|
||||
// final return nil
|
||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
|
||||
@@ -386,6 +399,24 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
}
|
||||
|
||||
if tbl.IsWithoutRowid {
|
||||
if hasCreatedAt {
|
||||
// Auto-timestamps: created_at. Don't overwrite existing timestamps (e.g., data import / migrations)
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{
|
||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")},
|
||||
Sel: ast.NewIdent("IsZero"),
|
||||
}},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
ret = append(ret, namedExecStmt(upsertStmt)...)
|
||||
ret = append(ret, PanicIfRowsAffected(tbl))
|
||||
} else {
|
||||
@@ -403,10 +434,21 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
ret1 := []ast.Stmt{Comment("Do create")}
|
||||
if hasCreatedAt {
|
||||
// Auto-timestamps: created_at
|
||||
ret1 = append(ret1, &ast.AssignStmt{
|
||||
ret1 = append(ret1, &ast.IfStmt{
|
||||
// Don't overwrite existing timestamps. This is useful for various reasons, e.g., data import / migrations
|
||||
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{
|
||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")},
|
||||
Sel: ast.NewIdent("IsZero"),
|
||||
}},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return append(ret1, namedExecStmt(insertStmt)...)
|
||||
@@ -477,7 +519,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
||||
for _, col := range cols {
|
||||
funcParam := ast.NewIdent(col.LongGoVarName())
|
||||
funcParams.List = append(funcParams.List, &ast.Field{Names: []*ast.Ident{funcParam}, Type: GoTypeForColumn(col)})
|
||||
colNames = append(colNames, fmt.Sprintf("%s = :%s", col.Name, col.Name))
|
||||
colNames = append(colNames, fmt.Sprintf("%s = ?", col.Name))
|
||||
funcNameSuffix = append(funcNameSuffix, col.GoFieldName())
|
||||
sqlParams = append(sqlParams, funcParam)
|
||||
}
|
||||
@@ -489,7 +531,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
||||
Y: SQLFieldsConstIdent(tbl),
|
||||
},
|
||||
Op: token.ADD,
|
||||
Y: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`\n\t from %s\n\t where %s = ?\n\t`", tbl.TableName, strings.Join(colNames, " and "))},
|
||||
Y: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`\n\t from %s\n\t where %s\n\t`", tbl.TableName, strings.Join(colNames, " and "))},
|
||||
}
|
||||
|
||||
return &ast.FuncDecl{
|
||||
@@ -515,7 +557,8 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
||||
&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")}}},
|
||||
Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}},
|
||||
},
|
||||
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.GoTypeName)}, ast.NewIdent("ErrNotInDB")}}}},
|
||||
},
|
||||
&ast.ReturnStmt{},
|
||||
|
||||
@@ -63,12 +63,52 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
testObj := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName))
|
||||
testObj2 := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName) + "2")
|
||||
fieldName := ast.NewIdent("Description") // TODO
|
||||
description1 := `"an item"`
|
||||
description2 := `"a big item"`
|
||||
description1 := "an item"
|
||||
description2 := "a big item"
|
||||
testDB := ast.NewIdent("TestDB")
|
||||
|
||||
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
|
||||
|
||||
makeDeepEqual := func(obj1 *ast.Ident, obj2 *ast.Ident) *ast.IfStmt {
|
||||
return &ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
&ast.Ident{Name: "diff"},
|
||||
},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "deep"},
|
||||
Sel: &ast.Ident{Name: "Equal"},
|
||||
},
|
||||
Args: []ast.Expr{obj1, obj2},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: &ast.Ident{Name: "diff"},
|
||||
Op: token.NEQ,
|
||||
Y: &ast.Ident{Name: "nil"},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: &ast.Ident{Name: "t"},
|
||||
Sel: &ast.Ident{Name: "Error"},
|
||||
},
|
||||
Args: []ast.Expr{
|
||||
&ast.Ident{Name: "diff"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
testFuncType := &ast.FuncType{
|
||||
Params: &ast.FieldList{
|
||||
List: []*ast.Field{{
|
||||
@@ -78,6 +118,103 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
}
|
||||
|
||||
// Generate FK Check test func first, because it also detects whether there are foreign keys
|
||||
shouldIncludeTestFkCheck := false
|
||||
testFkChecking := &ast.FuncDecl{
|
||||
Name: ast.NewIdent("Test" + tbl.GoTypeName + "FkChecking"),
|
||||
Type: testFuncType,
|
||||
Body: &ast.BlockStmt{
|
||||
List: func() (stmts []ast.Stmt) {
|
||||
isFirst := true
|
||||
for _, col := range tbl.Columns {
|
||||
if !col.IsForeignKey {
|
||||
continue
|
||||
}
|
||||
shouldIncludeTestFkCheck = true
|
||||
|
||||
// post := MakePost()
|
||||
if !isFirst {
|
||||
stmts = append(stmts, BlankLine())
|
||||
}
|
||||
|
||||
stmts = append(stmts, []ast.Stmt{
|
||||
// Comment header
|
||||
Comment(fmt.Sprintf("Invalid %s", col.GoFieldName())),
|
||||
|
||||
// `Invalid
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent(tbl.VarName)},
|
||||
Tok: map[bool]token.Token{true: token.DEFINE, false: token.ASSIGN}[isFirst],
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("Make" + tbl.GoTypeName),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// `post.QuotedPostID = 94354538969386985`
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
&ast.SelectorExpr{
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
Sel: ast.NewIdent(col.GoFieldName()),
|
||||
},
|
||||
},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.BasicLit{
|
||||
Kind: token.INT,
|
||||
Value: "94354538969386985",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// `err := db.SavePost(&post)`
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: map[bool]token.Token{true: token.DEFINE, false: token.ASSIGN}[isFirst],
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: testDB,
|
||||
Sel: ast.NewIdent("Save" + tbl.GoTypeName),
|
||||
},
|
||||
Args: []ast.Expr{
|
||||
&ast.UnaryExpr{
|
||||
Op: token.AND,
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// `assertForeignKeyError(t, err, "QuotedPostID", post.QuotedPostID)`
|
||||
&ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: ast.NewIdent("AssertForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
ast.NewIdent("t"),
|
||||
ast.NewIdent("err"),
|
||||
&ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: fmt.Sprintf("%q", col.GoFieldName()),
|
||||
},
|
||||
&ast.SelectorExpr{
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
Sel: ast.NewIdent(col.GoFieldName()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...)
|
||||
isFirst = false
|
||||
}
|
||||
return stmts
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
testCreateUpdateDelete := &ast.FuncDecl{
|
||||
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
|
||||
Type: testFuncType,
|
||||
@@ -116,11 +253,21 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
},
|
||||
|
||||
// TestDB.SaveItem(&item)
|
||||
&ast.ExprStmt{X: &ast.CallExpr{
|
||||
// TestDB.SaveItem(&item), possibly with error check
|
||||
&ast.ExprStmt{X: func() *ast.CallExpr {
|
||||
mainExpr := &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent("Save" + tbl.GoTypeName)},
|
||||
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
||||
}},
|
||||
}
|
||||
if shouldIncludeTestFkCheck {
|
||||
// Also a check for whether the Save function returns an error
|
||||
return &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("require"), Sel: ast.NewIdent("NoError")},
|
||||
Args: []ast.Expr{ast.NewIdent("t"), mainExpr},
|
||||
}
|
||||
}
|
||||
return mainExpr
|
||||
}()},
|
||||
|
||||
// require.NotZero(t, item.ID)
|
||||
&ast.ExprStmt{X: &ast.CallExpr{
|
||||
@@ -151,15 +298,8 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
})},
|
||||
},
|
||||
|
||||
// assert.Equal(t, item.Description, item2.Description)
|
||||
&ast.ExprStmt{X: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
||||
Args: []ast.Expr{
|
||||
ast.NewIdent("t"),
|
||||
&ast.SelectorExpr{X: testObj, Sel: fieldName},
|
||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||
},
|
||||
}},
|
||||
// if deep.Equal(...) {...}
|
||||
makeDeepEqual(testObj, testObj2),
|
||||
)
|
||||
|
||||
stmts = append(stmts,
|
||||
@@ -170,7 +310,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: fieldName}},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: description2}},
|
||||
Rhs: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", description2)}},
|
||||
},
|
||||
|
||||
// TestDB.SaveItem(&item)
|
||||
@@ -189,15 +329,8 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
})},
|
||||
},
|
||||
|
||||
// assert.Equal(t, item.Description, item2.Description)
|
||||
&ast.ExprStmt{X: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
||||
Args: []ast.Expr{
|
||||
ast.NewIdent("t"),
|
||||
&ast.SelectorExpr{X: testObj, Sel: fieldName},
|
||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||
},
|
||||
}},
|
||||
// if deep.Equal(...) {...}
|
||||
makeDeepEqual(testObj, testObj2),
|
||||
)
|
||||
|
||||
indexGets, hasIndexedGets := []ast.Stmt{
|
||||
@@ -292,93 +425,6 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
}
|
||||
|
||||
shouldIncludeTestFkCheck := false
|
||||
testFkChecking := &ast.FuncDecl{
|
||||
Name: ast.NewIdent("Test" + tbl.GoTypeName + "FkChecking"),
|
||||
Type: testFuncType,
|
||||
Body: &ast.BlockStmt{
|
||||
List: func() []ast.Stmt {
|
||||
// post := MakePost()
|
||||
stmts := []ast.Stmt{
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent(tbl.VarName)},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("Make" + tbl.GoTypeName),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
shouldDefineErr := true
|
||||
for _, col := range tbl.Columns {
|
||||
if col.IsForeignKey {
|
||||
shouldIncludeTestFkCheck = true
|
||||
stmts = append(stmts, []ast.Stmt{
|
||||
// post.QuotedPostID = 94354538969386985
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
&ast.SelectorExpr{
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
Sel: ast.NewIdent(col.GoFieldName()),
|
||||
},
|
||||
},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.BasicLit{
|
||||
Kind: token.INT,
|
||||
Value: "94354538969386985",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// err := db.SavePost(&post)
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: map[bool]token.Token{true: token.DEFINE, false: token.ASSIGN}[shouldDefineErr],
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
X: testDB,
|
||||
Sel: ast.NewIdent("Save" + tbl.GoTypeName),
|
||||
},
|
||||
Args: []ast.Expr{
|
||||
&ast.UnaryExpr{
|
||||
Op: token.AND,
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// assertForeignKeyError(t, err, "QuotedPostID", post.QuotedPostID)
|
||||
&ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: ast.NewIdent("AssertForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
ast.NewIdent("t"),
|
||||
ast.NewIdent("err"),
|
||||
&ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: fmt.Sprintf("%q", col.GoFieldName()),
|
||||
},
|
||||
&ast.SelectorExpr{
|
||||
X: ast.NewIdent(tbl.VarName),
|
||||
Sel: ast.NewIdent(col.GoFieldName()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...)
|
||||
shouldDefineErr = false
|
||||
}
|
||||
}
|
||||
return stmts
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
testList := []ast.Decl{
|
||||
makeItemFunc,
|
||||
testCreateUpdateDelete,
|
||||
@@ -406,6 +452,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
Path: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s/pkg/%s"`, gomodName, packageName)},
|
||||
Name: ast.NewIdent("."),
|
||||
},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"github.com/go-test/deep"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"github.com/stretchr/testify/assert"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"github.com/stretchr/testify/require"`}},
|
||||
},
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||
)
|
||||
|
||||
// Column represents a single column in a table.
|
||||
type Column struct {
|
||||
// TableName is the name of the SQLite table this column belongs to.
|
||||
TableName string `db:"table_name"`
|
||||
|
||||
// Name is the SQLite column name.
|
||||
Name string `db:"column_name"`
|
||||
|
||||
// Type is the SQLite type this column contains.
|
||||
Type string `db:"column_type"`
|
||||
|
||||
IsNotNull bool `db:"notnull"`
|
||||
HasDefaultValue bool `db:"has_default_value"`
|
||||
DefaultValue string `db:"dflt_value"`
|
||||
IsPrimaryKey bool `db:"is_primary_key"`
|
||||
PrimaryKeyRank uint `db:"primary_key_rank"`
|
||||
IsForeignKey bool `db:"is_foreign_key"`
|
||||
ForeignKeyTargetTable string `db:"fk_target_table"`
|
||||
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
||||
}
|
||||
|
||||
// IsNullableForeignKey is a helper function.
|
||||
func (c Column) IsNullableForeignKey() bool {
|
||||
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
||||
}
|
||||
|
||||
func (c Column) IsNonCodeTableForeignKey() bool {
|
||||
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
||||
}
|
||||
|
||||
func (c Column) GoFieldName() string {
|
||||
if c.Name == "rowid" {
|
||||
return "ID"
|
||||
}
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
||||
}
|
||||
return textutils.SnakeToCamel(c.Name)
|
||||
}
|
||||
|
||||
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
||||
func (c Column) GoVarName() string {
|
||||
if c.Name == "rowid" {
|
||||
return strings.ToLower(c.TableName)[0:1] + "ID"
|
||||
// TODO: Or should it just be "id"??
|
||||
}
|
||||
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||
}
|
||||
|
||||
// Otherwise, just use the whole name
|
||||
return c.LongGoVarName()
|
||||
}
|
||||
|
||||
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
||||
func (c Column) LongGoVarName() string {
|
||||
return textutils.CamelToPascal(c.GoFieldName())
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package schema
|
||||
|
||||
type Index struct {
|
||||
Name string `db:"index_name"`
|
||||
TableName string `db:"table_name"`
|
||||
Columns []string
|
||||
IsUnique bool `db:"is_unique"`
|
||||
// TODO: `where ...` for partial indexes
|
||||
// TODO: identify columns that are expressions
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package schema
|
||||
|
||||
type Schema struct {
|
||||
Tables map[string]Table
|
||||
Indexes map[string]Index
|
||||
}
|
||||
@@ -1,14 +1,66 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||
)
|
||||
|
||||
// Column represents a single column in a table.
|
||||
type Column struct {
|
||||
TableName string `db:"table_name"`
|
||||
Name string `db:"column_name"`
|
||||
Type string `db:"column_type"`
|
||||
IsNotNull bool `db:"notnull"`
|
||||
HasDefaultValue bool `db:"has_default_value"`
|
||||
DefaultValue string `db:"dflt_value"`
|
||||
IsPrimaryKey bool `db:"is_primary_key"`
|
||||
PrimaryKeyRank uint `db:"primary_key_rank"`
|
||||
IsForeignKey bool `db:"is_foreign_key"`
|
||||
ForeignKeyTargetTable string `db:"fk_target_table"`
|
||||
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
||||
}
|
||||
|
||||
// IsNullableForeignKey is a helper function.
|
||||
func (c Column) IsNullableForeignKey() bool {
|
||||
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
||||
}
|
||||
|
||||
func (c Column) IsNonCodeTableForeignKey() bool {
|
||||
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
||||
}
|
||||
|
||||
func (c Column) GoFieldName() string {
|
||||
if c.Name == "rowid" {
|
||||
return "ID"
|
||||
}
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
||||
}
|
||||
return textutils.SnakeToCamel(c.Name)
|
||||
}
|
||||
|
||||
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
||||
func (c Column) GoVarName() string {
|
||||
if c.Name == "rowid" {
|
||||
return strings.ToLower(c.TableName)[0:1] + "ID"
|
||||
// TODO: Or should it just be "id"??
|
||||
}
|
||||
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||
}
|
||||
|
||||
// Otherwise, just use the whole name
|
||||
return c.LongGoVarName()
|
||||
}
|
||||
|
||||
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
||||
func (c Column) LongGoVarName() string {
|
||||
return textutils.CamelToPascal(c.GoFieldName())
|
||||
}
|
||||
|
||||
// Table is a single SQLite table.
|
||||
type Table struct {
|
||||
TableName string `db:"name"`
|
||||
@@ -67,10 +119,16 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (t Table) GetCodeTableValues(db *sqlx.DB) (ret []string) {
|
||||
if !slices.ContainsFunc(t.Columns, func(c Column) bool { return c.Name == "name" }) {
|
||||
panic("not a code table")
|
||||
type Index struct {
|
||||
Name string `db:"index_name"`
|
||||
TableName string `db:"table_name"`
|
||||
Columns []string
|
||||
IsUnique bool `db:"is_unique"`
|
||||
// TODO: `where ...` for partial indexes
|
||||
// TODO: identify columns that are expressions
|
||||
}
|
||||
flowutils.PanicIf(db.Select(&ret, fmt.Sprintf("select name from %s", t.TableName)))
|
||||
return
|
||||
|
||||
type Schema struct {
|
||||
Tables map[string]Table
|
||||
Indexes map[string]Index
|
||||
}
|
||||
|
||||
@@ -10,16 +10,6 @@ func SnakeToCamel(s string) string {
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func KebabToPascal(s string) string {
|
||||
parts := strings.Split(s, "-")
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func CamelToPascal(s string) string {
|
||||
return strings.ToLower(s)[0:1] + s[1:]
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
create table item_types (
|
||||
rowid integer primary key,
|
||||
name text not null unique
|
||||
) strict;
|
||||
insert into item_types(rowid, name) values
|
||||
(1, 'first-type'),
|
||||
(2, 'second-type'),
|
||||
(3, 'third-type');
|
||||
Reference in New Issue
Block a user