Compare commits
20 Commits
e53546a7f5
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 616304c7dd | |||
| 17fc8a68f6 | |||
| d572745613 | |||
| 9bfb31798c | |||
| eafeb658bd | |||
| dbf14e23b6 | |||
| ad1782c73d | |||
| ccd7e32cbf | |||
| ed4ade1956 | |||
| 9a11f3986c | |||
| 8c29d455ff | |||
| 0f9a57dd85 | |||
| 11fed4b9c7 | |||
| 4e0836eb2e | |||
| f82929f6e2 | |||
| 1bc7f9111f | |||
| c1150954e5 | |||
| fd90830340 | |||
| fcf266eb1d | |||
| 29787b5521 |
@@ -1,6 +1,6 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
on: [push, workflow_dispatch]
|
||||
|
||||
jobs:
|
||||
# These steps build the `gas` docker image.
|
||||
|
||||
48
README.md
Normal file
48
README.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# GAS stack
|
||||
|
||||
|
||||
|
||||
## Compiling
|
||||
|
||||
Requires a Go compiler (minimum 1.22.5) and a C compiler, due to use of CGo.
|
||||
|
||||
```sh
|
||||
git clone https://git.offline-twitter.com/offline-labs/gas-stack.git
|
||||
cd gas-stack
|
||||
go build -o gas -tags fts5 ./cmd
|
||||
|
||||
# Installation (optional)
|
||||
sudo mv gas /usr/local/bin # ...or anywhere on your $PATH
|
||||
which gas # should print "/usr/local/bin/gas"
|
||||
```
|
||||
|
||||
## Using
|
||||
|
||||
The linter (`gas sqlite_lint`) is stable and useful.
|
||||
|
||||
The code generator is buggy, incomplete, and not remotely stable, but still quite useful. Don't expect it to produce perfectly working code, or even to compile correctly (e.g., you'll probably have to fix the imports). Copy-paste the parts that are useful, and delete the parts that aren't.
|
||||
|
||||
|
||||
#### Linter
|
||||
|
||||
```sh
|
||||
gas sqlite_lint <path/to/schema.sql>
|
||||
```
|
||||
|
||||
#### Code generator
|
||||
|
||||
```sh
|
||||
gas generate table_name # Generates a model
|
||||
gas generate --test table_name # Optional: generates tests
|
||||
```
|
||||
|
||||
It prints to the console. You can copy-paste the result. Or you can use bash redirection:
|
||||
|
||||
```sh
|
||||
gas generate users > pkg/db/user.go
|
||||
gas generate --test users > pkg/db/user_test.go
|
||||
```
|
||||
|
||||
Useful flags:
|
||||
|
||||
- `--schema`: by default, `gas generate` assumes that the schema is at `pkg/db/schema.sql`. Use `gas generate --schema <path/to/schema.sql> [...]` to indicate otherwise
|
||||
@@ -46,7 +46,6 @@ var generate_model = &cobra.Command{
|
||||
Specs: []ast.Spec{
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"database/sql"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"errors"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
||||
&ast.ImportSpec{
|
||||
Name: ast.NewIdent("."),
|
||||
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"`},
|
||||
@@ -69,8 +68,16 @@ var generate_model = &cobra.Command{
|
||||
modelgenerate.GenerateSQLFieldsConst(table),
|
||||
modelgenerate.GenerateSaveItemFunc(table),
|
||||
modelgenerate.GenerateDeleteItemFunc(table),
|
||||
modelgenerate.GenerateGetItemByIDFunc(table),
|
||||
)
|
||||
if table.IsWithoutRowid {
|
||||
decls = append(decls,
|
||||
modelgenerate.GenerateGetItemBy(table, table.PrimaryKeyColumns()),
|
||||
)
|
||||
} else {
|
||||
decls = append(decls,
|
||||
modelgenerate.GenerateGetItemByIDFunc(table),
|
||||
)
|
||||
}
|
||||
for _, index := range schema.Indexes {
|
||||
if index.TableName != table.TableName {
|
||||
// Skip indexes on other tables
|
||||
|
||||
@@ -41,15 +41,24 @@ create table items (
|
||||
rowid integer primary key,
|
||||
description text not null default '',
|
||||
flavor integer references item_flavor(rowid),
|
||||
data blob not null,
|
||||
thing text not null unique,
|
||||
created_at integer not null,
|
||||
updated_at integer not null
|
||||
) strict;
|
||||
|
||||
create table item_to_item (
|
||||
item1_id integer references items(rowid),
|
||||
item2_id integer references items(rowid),
|
||||
primary key (item1_id, item2_id)
|
||||
) strict, without rowid;
|
||||
|
||||
EOF
|
||||
|
||||
# Generate an item model and test file
|
||||
$gas generate items > pkg/db/item.go
|
||||
$gas generate items --test > pkg/db/item_test.go
|
||||
$gas generate item_to_item > pkg/db/item_to_item.go
|
||||
go mod tidy
|
||||
|
||||
# Run the tests
|
||||
|
||||
@@ -76,28 +76,35 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
||||
return fmt.Errorf("re-parsing pretty-print: %w", err)
|
||||
}
|
||||
|
||||
// Convert the tree-of-nodes into a slice-of-nodes
|
||||
collectNodes := func(node ast.Node) []ast.Node {
|
||||
var nodes []ast.Node
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Filter out comments, as they only appear in the
|
||||
switch n.(type) {
|
||||
case *ast.CommentGroup, *ast.Comment:
|
||||
return false
|
||||
}
|
||||
nodes = append(nodes, n)
|
||||
return true
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Parallel walk: apply TrailingComments from the side map.
|
||||
// Both trees have identical structure (the reparse is just a positioned copy),
|
||||
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
||||
// avoid mismatches from Doc fields.
|
||||
if len(TrailingComments) > 0 {
|
||||
// Helper: convert the tree-of-nodes into a slice-of-nodes
|
||||
collectNodes := func(node ast.Node) []ast.Node {
|
||||
var nodes []ast.Node
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
// Filter out comments, as they only appear in the
|
||||
switch n.(type) {
|
||||
case *ast.CommentGroup, *ast.Comment:
|
||||
return false
|
||||
}
|
||||
nodes = append(nodes, n)
|
||||
return true
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
origNodes := collectNodes(file)
|
||||
reparsedNodes := collectNodes(parsed)
|
||||
if len(origNodes) != len(reparsedNodes) {
|
||||
panic(fmt.Sprintf(
|
||||
"origNodes: %d; reparsedNodes: %d. The AST generator is likely generating an invalid AST",
|
||||
len(origNodes), len(reparsedNodes),
|
||||
))
|
||||
}
|
||||
|
||||
for i, orig := range origNodes {
|
||||
text, isOk := TrailingComments[orig]
|
||||
if !isOk {
|
||||
|
||||
@@ -18,15 +18,57 @@ import (
|
||||
// ---------------
|
||||
|
||||
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")}
|
||||
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")}
|
||||
)
|
||||
|
||||
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
||||
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
|
||||
}
|
||||
|
||||
// GoTypeForColumn returns a type expression for this column.
|
||||
//
|
||||
// For most columns this isjust its mapped name as a `ast.NewIdent`, but for "blob" it needs
|
||||
// a slice expression (`[]byte`).
|
||||
func GoTypeForColumn(c schema.Column) ast.Expr {
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return ast.NewIdent(schema.TypenameFromTablename(c.ForeignKeyTargetTable) + "ID")
|
||||
}
|
||||
switch c.Type {
|
||||
case "integer", "int":
|
||||
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
|
||||
return ast.NewIdent("bool")
|
||||
} else if strings.HasSuffix(c.Name, "_at") {
|
||||
return ast.NewIdent("Timestamp")
|
||||
}
|
||||
return ast.NewIdent("int")
|
||||
case "text":
|
||||
return ast.NewIdent("string")
|
||||
case "real":
|
||||
return ast.NewIdent("float32")
|
||||
case "blob":
|
||||
return &ast.ArrayType{Elt: ast.NewIdent("byte")}
|
||||
default:
|
||||
panic("Unrecognized sqlite column type: " + c.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func PanicIfRowsAffected(tbl schema.Table) *ast.IfStmt {
|
||||
return &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
|
||||
Args: []ast.Expr{},
|
||||
}),
|
||||
Op: token.NEQ,
|
||||
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.NewIdent(tbl.VarName)}}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Generators
|
||||
// ---------------
|
||||
@@ -63,10 +105,9 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||
})
|
||||
} else {
|
||||
typeName := col.GoTypeName()
|
||||
fields = append(fields, &ast.Field{
|
||||
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))},
|
||||
Type: ast.NewIdent(typeName),
|
||||
Type: GoTypeForColumn(col),
|
||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||
})
|
||||
}
|
||||
@@ -120,79 +161,92 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||
structFieldName := col.GoFieldName()
|
||||
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
||||
|
||||
if col.IsNonCodeTableForeignKey() {
|
||||
// Real foreign key; look up referent by ID to see if it exists
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent(getByIDFuncName(col.ForeignKeyTargetTable))},
|
||||
Args: []ast.Expr{structField},
|
||||
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
|
||||
return wrap(&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent(getByIDFuncName(col.ForeignKeyTargetTable))},
|
||||
Args: []ast.Expr{structField},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cond: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||
Args: []ast.Expr{ast.NewIdent("err"), ast.NewIdent("ErrNotInDB")},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||
structField,
|
||||
Cond: &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||
Args: []ast.Expr{ast.NewIdent("err"), ast.NewIdent("ErrNotInDB")},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||
structField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Code table value. Query the table to see if it exists
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
|
||||
Args: []ast.Expr{
|
||||
&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)},
|
||||
structField,
|
||||
})
|
||||
} else {
|
||||
// Code table value. Query the table to see if it exists
|
||||
return wrap(&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
|
||||
Args: []ast.Expr{
|
||||
&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)},
|
||||
structField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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")}},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)),
|
||||
structField,
|
||||
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")}},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||
structField,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}())
|
||||
}
|
||||
// final return nil
|
||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
|
||||
@@ -229,10 +283,29 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
if col.Name == "created_at" && hasCreatedAt {
|
||||
continue
|
||||
}
|
||||
updatePairs = append(updatePairs, col.Name+"="+val)
|
||||
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, ", "),
|
||||
)
|
||||
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 "),
|
||||
)
|
||||
upsertStmt := fmt.Sprintf("\n\t insert into %s (%s)\n\t values (%s)\n\t",
|
||||
tbl.TableName,
|
||||
strings.Join(insertCols, ", "),
|
||||
strings.Join(insertVals, ", "),
|
||||
)
|
||||
if len(updatePairs) == 0 {
|
||||
upsertStmt = upsertStmt + " on conflict do nothing\n\t"
|
||||
} else {
|
||||
upsertStmt = upsertStmt + fmt.Sprintf(" on conflict do update\n\t set %s\n\t", strings.Join(updatePairs, ",\n\t "))
|
||||
}
|
||||
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, ", "))
|
||||
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 "))
|
||||
|
||||
checkForeignKeyFailuresAssignment, hasFks := buildFKCheckLambda(tbl)
|
||||
|
||||
@@ -250,139 +323,161 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||
})
|
||||
}
|
||||
// if item.ID == 0 {...} else {...}
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
Op: token.EQL,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
// Do create
|
||||
List: append(
|
||||
func() []ast.Stmt {
|
||||
ret1 := []ast.Stmt{Comment("Do create")}
|
||||
if hasCreatedAt {
|
||||
// Auto-timestamps: created_at
|
||||
ret1 = append(ret1, &ast.AssignStmt{
|
||||
|
||||
namedExecStmt := func(stmt string) []ast.Stmt {
|
||||
queryStmt := &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + stmt + "`"},
|
||||
ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
}
|
||||
if !hasFks {
|
||||
// No foreign key checking needed; just use `Must` for brevity
|
||||
return []ast.Stmt{&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{mustCall(queryStmt)},
|
||||
}}
|
||||
}
|
||||
// There's foreign keys
|
||||
return []ast.Stmt{
|
||||
// result, err := db.DB.NamedExec(`...`, u)
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
ast.NewIdent("result"),
|
||||
ast.NewIdent("err"),
|
||||
},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{queryStmt},
|
||||
},
|
||||
// if fkErr := checkForeignKeyFailures(err); fkErr != nil { return fkErr } else if err != nil { panic(err) }
|
||||
&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("fkErr")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("checkForeignKeyFailures"),
|
||||
Args: []ast.Expr{ast.NewIdent("err")},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: ast.NewIdent("fkErr"),
|
||||
Op: token.NEQ,
|
||||
Y: ast.NewIdent("nil"),
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{ast.NewIdent("fkErr")},
|
||||
},
|
||||
},
|
||||
},
|
||||
Else: func() *ast.IfStmt {
|
||||
panicStmt := &ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: ast.NewIdent("panic"),
|
||||
Args: []ast.Expr{ast.NewIdent("err")},
|
||||
},
|
||||
}
|
||||
TrailingComments[panicStmt] = "not a foreign key error"
|
||||
return &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: ast.NewIdent("err"),
|
||||
Op: token.NEQ,
|
||||
Y: ast.NewIdent("nil"),
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{panicStmt},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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{}}},
|
||||
})
|
||||
}
|
||||
namedExecStmt := &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
||||
ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
}
|
||||
if !hasFks {
|
||||
// No foreign key checking needed; just use `Must` for brevity
|
||||
return append(ret1, &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{mustCall(namedExecStmt)},
|
||||
})
|
||||
}
|
||||
|
||||
return append(ret1,
|
||||
// result, err := db.DB.NamedExec(`...`, u)
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
ast.NewIdent("result"),
|
||||
ast.NewIdent("err"),
|
||||
},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{namedExecStmt},
|
||||
},
|
||||
|
||||
// if fkErr := checkForeignKeyFailures(err); fkErr != nil { return fkErr } else if err != nil { panic(err) }
|
||||
&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("fkErr")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: ast.NewIdent("checkForeignKeyFailures"),
|
||||
Args: []ast.Expr{ast.NewIdent("err")},
|
||||
},
|
||||
},
|
||||
},
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: ast.NewIdent("fkErr"),
|
||||
Op: token.NEQ,
|
||||
Y: ast.NewIdent("nil"),
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.ReturnStmt{
|
||||
Results: []ast.Expr{ast.NewIdent("fkErr")},
|
||||
},
|
||||
},
|
||||
},
|
||||
Else: func() *ast.IfStmt {
|
||||
panicStmt := &ast.ExprStmt{
|
||||
X: &ast.CallExpr{
|
||||
Fun: ast.NewIdent("panic"),
|
||||
Args: []ast.Expr{ast.NewIdent("err")},
|
||||
},
|
||||
}
|
||||
TrailingComments[panicStmt] = "not a foreign key error"
|
||||
return &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: ast.NewIdent("err"),
|
||||
Op: token.NEQ,
|
||||
Y: ast.NewIdent("nil"),
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{panicStmt},
|
||||
},
|
||||
}
|
||||
}(),
|
||||
},
|
||||
)
|
||||
}(),
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.CallExpr{
|
||||
Fun: ast.NewIdent(tbl.TypeIDName),
|
||||
Args: []ast.Expr{mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")},
|
||||
Args: []ast.Expr{},
|
||||
})},
|
||||
}},
|
||||
},
|
||||
),
|
||||
},
|
||||
Else: &ast.BlockStmt{
|
||||
// Do update
|
||||
List: []ast.Stmt{
|
||||
Comment("Do update"),
|
||||
&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)},
|
||||
})},
|
||||
},
|
||||
|
||||
&ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
|
||||
Args: []ast.Expr{},
|
||||
}),
|
||||
Op: token.NEQ,
|
||||
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: 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")}}}}}}}},
|
||||
},
|
||||
})
|
||||
}
|
||||
ret = append(ret, namedExecStmt(upsertStmt)...)
|
||||
ret = append(ret, PanicIfRowsAffected(tbl))
|
||||
} else {
|
||||
// if item.ID == 0 {...} else {...}
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
Op: token.EQL,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
||||
},
|
||||
},
|
||||
})
|
||||
Body: &ast.BlockStmt{
|
||||
// Do create
|
||||
List: append(
|
||||
func() []ast.Stmt {
|
||||
ret1 := []ast.Stmt{Comment("Do create")}
|
||||
if hasCreatedAt {
|
||||
// Auto-timestamps: created_at
|
||||
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)...)
|
||||
}(),
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.CallExpr{
|
||||
Fun: ast.NewIdent(tbl.TypeIDName),
|
||||
Args: []ast.Expr{mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")},
|
||||
Args: []ast.Expr{},
|
||||
})},
|
||||
}},
|
||||
},
|
||||
),
|
||||
},
|
||||
Else: &ast.BlockStmt{
|
||||
// Do update
|
||||
List: append(
|
||||
[]ast.Stmt{Comment("Do update")},
|
||||
append(
|
||||
namedExecStmt(updateStmt),
|
||||
PanicIfRowsAffected(tbl),
|
||||
)...,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasFks {
|
||||
// If there's foreign key checking, it needs to return an error (or nil)
|
||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
|
||||
@@ -416,6 +511,62 @@ func getByIDFuncName(tblname string) string {
|
||||
return "Get" + schema.TypenameFromTablename(tblname) + "ByID"
|
||||
}
|
||||
|
||||
func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
||||
colNames := []string{}
|
||||
funcNameSuffix := []string{}
|
||||
funcParams := &ast.FieldList{List: []*ast.Field{}}
|
||||
sqlParams := []ast.Expr{}
|
||||
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 = ?", col.Name))
|
||||
funcNameSuffix = append(funcNameSuffix, col.GoFieldName())
|
||||
sqlParams = append(sqlParams, funcParam)
|
||||
}
|
||||
|
||||
selectExpr := &ast.BinaryExpr{
|
||||
X: &ast.BinaryExpr{
|
||||
X: &ast.BasicLit{Kind: token.STRING, Value: "`\n\t select `"},
|
||||
Op: token.ADD,
|
||||
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 "))},
|
||||
}
|
||||
|
||||
return &ast.FuncDecl{
|
||||
Recv: dbRecv,
|
||||
Name: ast.NewIdent(fmt.Sprintf("Get%sBy%s", schema.TypenameFromTablename(tbl.TableName), strings.Join(funcNameSuffix, "And"))),
|
||||
Type: &ast.FuncType{
|
||||
Params: funcParams,
|
||||
Results: &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")},
|
||||
}},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
|
||||
Args: append([]ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, selectExpr}, sqlParams...),
|
||||
}},
|
||||
},
|
||||
&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")}},
|
||||
},
|
||||
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.GoTypeName)}, ast.NewIdent("ErrNotInDB")}}}},
|
||||
},
|
||||
&ast.ReturnStmt{},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function.
|
||||
// E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function.
|
||||
func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
@@ -448,13 +599,12 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
},
|
||||
}
|
||||
|
||||
funcDecl := &ast.FuncDecl{
|
||||
return &ast.FuncDecl{
|
||||
Recv: dbRecv,
|
||||
Name: ast.NewIdent(getByIDFuncName(tbl.TableName)),
|
||||
Type: &ast.FuncType{Params: arg, Results: result},
|
||||
Body: funcBody,
|
||||
}
|
||||
return funcDecl
|
||||
}
|
||||
|
||||
// GenerateGetItemByUniqColFunc produces an AST for the `GetXyzByID()` function.
|
||||
@@ -478,7 +628,7 @@ func GenerateGetItemByUniqColFunc(tbl schema.Table, col schema.Column) *ast.Func
|
||||
Name: ast.NewIdent("Get" + schema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName()),
|
||||
Type: &ast.FuncType{
|
||||
Params: &ast.FieldList{List: []*ast.Field{
|
||||
{Names: []*ast.Ident{param}, Type: ast.NewIdent(col.GoTypeName())},
|
||||
{Names: []*ast.Ident{param}, Type: GoTypeForColumn(col)},
|
||||
}},
|
||||
Results: &ast.FieldList{List: []*ast.Field{
|
||||
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)},
|
||||
@@ -555,10 +705,12 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
// GenerateDeleteItemFunc produces an AST for the `DeleteXyz()` function.
|
||||
// E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function.
|
||||
func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
arg := &ast.FieldList{List: []*ast.Field{{
|
||||
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
}}}
|
||||
colNames := []string{}
|
||||
for _, c := range tbl.PrimaryKeyColumns() {
|
||||
colNames = append(colNames, fmt.Sprintf("%s = :%s", c.Name, c.Name))
|
||||
}
|
||||
|
||||
sqlStr := "`delete from " + tbl.TableName + fmt.Sprintf(" where %s`", strings.Join(colNames, " and "))
|
||||
|
||||
funcBody := &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
@@ -566,41 +718,24 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Exec")},
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
|
||||
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: sqlStr},
|
||||
ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
})},
|
||||
},
|
||||
&ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: mustCall(
|
||||
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}},
|
||||
),
|
||||
Op: token.NEQ,
|
||||
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: fmtErrorf,
|
||||
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.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
PanicIfRowsAffected(tbl),
|
||||
},
|
||||
}
|
||||
|
||||
funcDecl := &ast.FuncDecl{
|
||||
Recv: dbRecv,
|
||||
Name: ast.NewIdent("Delete" + tbl.GoTypeName),
|
||||
Type: &ast.FuncType{Params: arg, Results: nil},
|
||||
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{
|
||||
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
}}}, Results: nil},
|
||||
Body: funcBody,
|
||||
}
|
||||
return funcDecl
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/jinzhu/inflection"
|
||||
|
||||
pkgschema "git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||
)
|
||||
|
||||
// GenerateModelTestAST produces an AST for a starter test file for a given model.
|
||||
@@ -15,9 +16,11 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
packageName := "db"
|
||||
testpackageName := packageName + "_test"
|
||||
|
||||
makeHelperName := ast.NewIdent("Make" + tbl.GoTypeName)
|
||||
|
||||
// func MakeItem() Item { return Item{} }
|
||||
makeItemFunc := &ast.FuncDecl{
|
||||
Name: ast.NewIdent("Make" + tbl.GoTypeName),
|
||||
Name: makeHelperName,
|
||||
Type: &ast.FuncType{
|
||||
Params: &ast.FieldList{},
|
||||
Results: &ast.FieldList{
|
||||
@@ -32,6 +35,24 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
Results: []ast.Expr{
|
||||
&ast.CompositeLit{
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
Elts: []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: ast.NewIdent("Data"),
|
||||
Value: &ast.CompositeLit{
|
||||
Type: &ast.ArrayType{
|
||||
Elt: ast.NewIdent("byte"),
|
||||
},
|
||||
Elts: []ast.Expr{},
|
||||
},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: ast.NewIdent("Description"),
|
||||
Value: &ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: `""`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -39,15 +60,55 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
}
|
||||
|
||||
testObj := ast.NewIdent("item")
|
||||
testObj2 := ast.NewIdent("item2")
|
||||
fieldName := ast.NewIdent("Description")
|
||||
description1 := `"an item"`
|
||||
description2 := `"a big item"`
|
||||
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"
|
||||
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{{
|
||||
@@ -57,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,
|
||||
@@ -72,26 +230,44 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
stmts := []ast.Stmt{
|
||||
Comment("Create"),
|
||||
|
||||
// item := Item{Description: "an item"}
|
||||
// item := MakeItem()
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{testObj},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{&ast.CompositeLit{
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
Elts: []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: fieldName,
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: description1},
|
||||
},
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: makeHelperName, Args: nil}},
|
||||
},
|
||||
// item.Description = "an item"
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
&ast.SelectorExpr{
|
||||
X: testObj,
|
||||
Sel: ast.NewIdent("Description"),
|
||||
},
|
||||
}},
|
||||
},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: fmt.Sprintf("%q", description1),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// 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}},
|
||||
}},
|
||||
// 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{
|
||||
@@ -122,15 +298,8 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
})},
|
||||
},
|
||||
|
||||
// assert.Equal(t, "an item", 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.BasicLit{Kind: token.STRING, Value: description1},
|
||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||
},
|
||||
}},
|
||||
// if deep.Equal(...) {...}
|
||||
makeDeepEqual(testObj, testObj2),
|
||||
)
|
||||
|
||||
stmts = append(stmts,
|
||||
@@ -141,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)
|
||||
@@ -160,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{
|
||||
@@ -263,92 +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),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
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: token.DEFINE,
|
||||
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()),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}...)
|
||||
}
|
||||
}
|
||||
return stmts
|
||||
}(),
|
||||
},
|
||||
}
|
||||
|
||||
testList := []ast.Decl{
|
||||
makeItemFunc,
|
||||
testCreateUpdateDelete,
|
||||
@@ -376,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"`}},
|
||||
},
|
||||
|
||||
@@ -52,32 +52,13 @@ func (c Column) GoVarName() string {
|
||||
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||
}
|
||||
|
||||
// Otherwise, just lowercase the field name
|
||||
fieldname := c.GoFieldName()
|
||||
return strings.ToLower(fieldname)[0:1] + fieldname[1:]
|
||||
// Otherwise, just use the whole name
|
||||
return c.LongGoVarName()
|
||||
}
|
||||
|
||||
func (c Column) GoTypeName() string {
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return TypenameFromTablename(c.ForeignKeyTargetTable) + "ID"
|
||||
}
|
||||
switch c.Type {
|
||||
case "integer", "int":
|
||||
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
|
||||
return "bool"
|
||||
} else if strings.HasSuffix(c.Name, "_at") {
|
||||
return "Timestamp"
|
||||
}
|
||||
return "int"
|
||||
case "text":
|
||||
return "string"
|
||||
case "real":
|
||||
return "float32"
|
||||
case "blob":
|
||||
return "[]byte"
|
||||
default:
|
||||
panic("Unrecognized sqlite column type: " + c.Type)
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -9,3 +9,7 @@ func SnakeToCamel(s string) string {
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func CamelToPascal(s string) string {
|
||||
return strings.ToLower(s)[0:1] + s[1:]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user