Compare commits
21 Commits
c09a2fe1fb
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 616304c7dd | |||
| 17fc8a68f6 | |||
| d572745613 | |||
| 9bfb31798c | |||
| eafeb658bd | |||
| dbf14e23b6 | |||
| ad1782c73d | |||
| ccd7e32cbf | |||
| ed4ade1956 | |||
| 9a11f3986c | |||
| 8c29d455ff | |||
| 0f9a57dd85 | |||
| 11fed4b9c7 | |||
| 4e0836eb2e | |||
| f82929f6e2 | |||
| 1bc7f9111f | |||
| c1150954e5 | |||
| fd90830340 | |||
| fcf266eb1d | |||
| 29787b5521 | |||
| e53546a7f5 |
@@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on: [push, workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# These steps build the `gas` docker image.
|
# 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{
|
Specs: []ast.Spec{
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"database/sql"`}},
|
&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: `"errors"`}},
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
|
||||||
&ast.ImportSpec{
|
&ast.ImportSpec{
|
||||||
Name: ast.NewIdent("."),
|
Name: ast.NewIdent("."),
|
||||||
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"`},
|
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.GenerateSQLFieldsConst(table),
|
||||||
modelgenerate.GenerateSaveItemFunc(table),
|
modelgenerate.GenerateSaveItemFunc(table),
|
||||||
modelgenerate.GenerateDeleteItemFunc(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 {
|
for _, index := range schema.Indexes {
|
||||||
if index.TableName != table.TableName {
|
if index.TableName != table.TableName {
|
||||||
// Skip indexes on other tables
|
// Skip indexes on other tables
|
||||||
|
|||||||
@@ -41,15 +41,24 @@ create table items (
|
|||||||
rowid integer primary key,
|
rowid integer primary key,
|
||||||
description text not null default '',
|
description text not null default '',
|
||||||
flavor integer references item_flavor(rowid),
|
flavor integer references item_flavor(rowid),
|
||||||
|
data blob not null,
|
||||||
thing text not null unique,
|
thing text not null unique,
|
||||||
created_at integer not null,
|
created_at integer not null,
|
||||||
updated_at integer not null
|
updated_at integer not null
|
||||||
) strict;
|
) 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
|
EOF
|
||||||
|
|
||||||
# Generate an item model and test file
|
# Generate an item model and test file
|
||||||
$gas generate items > pkg/db/item.go
|
$gas generate items > pkg/db/item.go
|
||||||
$gas generate items --test > pkg/db/item_test.go
|
$gas generate items --test > pkg/db/item_test.go
|
||||||
|
$gas generate item_to_item > pkg/db/item_to_item.go
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# Run the tests
|
# Run the tests
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package modelgenerate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/parser"
|
"go/parser"
|
||||||
"go/printer"
|
"go/printer"
|
||||||
@@ -65,29 +66,14 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
|||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
fset := token.NewFileSet()
|
fset := token.NewFileSet()
|
||||||
if err := printer.Fprint(&buf, fset, file); err != nil {
|
if err := printer.Fprint(&buf, fset, file); err != nil {
|
||||||
return err
|
return fmt.Errorf("initial pretty-printing to get positioning: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-parse to get real positions (ParseComments preserves doc comments)
|
// Re-parse to get real positions (ParseComments preserves doc comments)
|
||||||
fset = token.NewFileSet()
|
fset = token.NewFileSet()
|
||||||
parsed, err := parser.ParseFile(fset, "", buf.Bytes(), parser.ParseComments)
|
parsed, err := parser.ParseFile(fset, "", buf.Bytes(), parser.ParseComments)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
// Parallel walk: apply TrailingComments from the side map.
|
||||||
@@ -95,11 +81,33 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
|||||||
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
||||||
// avoid mismatches from Doc fields.
|
// avoid mismatches from Doc fields.
|
||||||
if len(TrailingComments) > 0 {
|
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)
|
origNodes := collectNodes(file)
|
||||||
reparsedNodes := collectNodes(parsed)
|
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 {
|
for i, orig := range origNodes {
|
||||||
text, ok := TrailingComments[orig]
|
text, isOk := TrailingComments[orig]
|
||||||
if !ok {
|
if !isOk {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
reparsed := reparsedNodes[i]
|
reparsed := reparsedNodes[i]
|
||||||
@@ -110,40 +118,43 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extractCommentMarker := func(stmt ast.Stmt) (string, bool) {
|
extractCommentMarker := func(stmt ast.Stmt) (string, bool) {
|
||||||
expr, ok := stmt.(*ast.ExprStmt)
|
expr, isOk := stmt.(*ast.ExprStmt)
|
||||||
if !ok {
|
if !isOk {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
call, ok := expr.X.(*ast.CallExpr)
|
call, isOk := expr.X.(*ast.CallExpr)
|
||||||
if !ok {
|
if !isOk {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
ident, ok := call.Fun.(*ast.Ident)
|
ident, isOk := call.Fun.(*ast.Ident)
|
||||||
if !ok || ident.Name != commentMarker {
|
if !isOk || ident.Name != commentMarker {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
lit, isOk := call.Args[0].(*ast.BasicLit)
|
||||||
|
if !isOk {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
lit := call.Args[0].(*ast.BasicLit)
|
|
||||||
return lit.Value[1 : len(lit.Value)-1], true
|
return lit.Value[1 : len(lit.Value)-1], true
|
||||||
}
|
}
|
||||||
|
|
||||||
isBlankLineMarker := func(stmt ast.Stmt) bool {
|
isBlankLineMarker := func(stmt ast.Stmt) bool {
|
||||||
expr, ok := stmt.(*ast.ExprStmt)
|
expr, isOk := stmt.(*ast.ExprStmt)
|
||||||
if !ok {
|
if !isOk {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
call, ok := expr.X.(*ast.CallExpr)
|
call, isOk := expr.X.(*ast.CallExpr)
|
||||||
if !ok {
|
if !isOk {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
ident, ok := call.Fun.(*ast.Ident)
|
ident, isOk := call.Fun.(*ast.Ident)
|
||||||
return ok && ident.Name == blankLineMarker
|
return isOk && ident.Name == blankLineMarker
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert comment and blank-line markers
|
// Convert comment and blank-line markers
|
||||||
ast.Inspect(parsed, func(n ast.Node) bool {
|
ast.Inspect(parsed, func(n ast.Node) bool {
|
||||||
// We only care about Block nodes
|
// We only care about Block nodes
|
||||||
block, ok := n.(*ast.BlockStmt)
|
block, isOk := n.(*ast.BlockStmt)
|
||||||
if !ok {
|
if !isOk {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,7 +162,7 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
|||||||
// in the block statement's body with actual ones
|
// in the block statement's body with actual ones
|
||||||
filtered := block.List[:0]
|
filtered := block.List[:0]
|
||||||
for _, stmt := range block.List {
|
for _, stmt := range block.List {
|
||||||
if text, ok := extractCommentMarker(stmt); ok {
|
if text, isOk := extractCommentMarker(stmt); isOk {
|
||||||
// If it's a comment, add it to the fileset's list of Comments
|
// If it's a comment, add it to the fileset's list of Comments
|
||||||
parsed.Comments = append(parsed.Comments, &ast.CommentGroup{
|
parsed.Comments = append(parsed.Comments, &ast.CommentGroup{
|
||||||
List: []*ast.Comment{{Slash: stmt.Pos(), Text: "// " + text}},
|
List: []*ast.Comment{{Slash: stmt.Pos(), Text: "// " + text}},
|
||||||
@@ -176,5 +187,9 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
|||||||
delete(TrailingComments, k)
|
delete(TrailingComments, k)
|
||||||
}
|
}
|
||||||
|
|
||||||
return printer.Fprint(w, fset, parsed)
|
err = printer.Fprint(w, fset, parsed)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("re-pretty-printing: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,15 +18,57 @@ import (
|
|||||||
// ---------------
|
// ---------------
|
||||||
|
|
||||||
var (
|
var (
|
||||||
dbRecv = &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
|
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")}
|
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 {
|
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
||||||
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
|
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
|
// 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)},
|
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
typeName := col.GoTypeName()
|
|
||||||
fields = append(fields, &ast.Field{
|
fields = append(fields, &ast.Field{
|
||||||
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))},
|
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)},
|
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()
|
structFieldName := col.GoFieldName()
|
||||||
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
||||||
|
|
||||||
if col.IsNonCodeTableForeignKey() {
|
ret = append(ret, func() ast.Stmt {
|
||||||
// Real foreign key; look up referent by ID to see if it exists
|
// Wrap nullable FKs in "if a.val != 0 { ... }"
|
||||||
ret = append(ret, &ast.IfStmt{
|
wrap := func(input ast.Stmt) ast.Stmt {
|
||||||
Init: &ast.AssignStmt{
|
if col.IsNullableForeignKey() {
|
||||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
return &ast.IfStmt{
|
||||||
Tok: token.DEFINE,
|
Cond: &ast.BinaryExpr{X: structField, Op: token.NEQ, Y: &ast.BasicLit{Kind: token.INT, Value: "0"}},
|
||||||
Rhs: []ast.Expr{
|
Body: &ast.BlockStmt{List: []ast.Stmt{input}},
|
||||||
&ast.CallExpr{
|
}
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent(getByIDFuncName(col.ForeignKeyTargetTable))},
|
} else {
|
||||||
Args: []ast.Expr{structField},
|
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{
|
||||||
Cond: &ast.CallExpr{
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
Args: []ast.Expr{ast.NewIdent("err"), ast.NewIdent("ErrNotInDB")},
|
||||||
Args: []ast.Expr{ast.NewIdent("err"), ast.NewIdent("ErrNotInDB")},
|
},
|
||||||
},
|
Body: &ast.BlockStmt{
|
||||||
Body: &ast.BlockStmt{
|
List: []ast.Stmt{
|
||||||
List: []ast.Stmt{
|
&ast.ReturnStmt{
|
||||||
&ast.ReturnStmt{
|
Results: []ast.Expr{
|
||||||
Results: []ast.Expr{
|
&ast.CallExpr{
|
||||||
&ast.CallExpr{
|
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
Args: []ast.Expr{
|
||||||
Args: []ast.Expr{
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
structField,
|
||||||
structField,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
} else {
|
||||||
} else {
|
// Code table value. Query the table to see if it exists
|
||||||
// Code table value. Query the table to see if it exists
|
return wrap(&ast.IfStmt{
|
||||||
ret = append(ret, &ast.IfStmt{
|
Init: &ast.AssignStmt{
|
||||||
Init: &ast.AssignStmt{
|
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
Tok: token.DEFINE,
|
||||||
Tok: token.ASSIGN,
|
Rhs: []ast.Expr{
|
||||||
Rhs: []ast.Expr{
|
&ast.CallExpr{
|
||||||
&ast.CallExpr{
|
Fun: &ast.SelectorExpr{X: dbDB, 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)},
|
structField,
|
||||||
structField,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
Cond: &ast.CallExpr{
|
||||||
Cond: &ast.CallExpr{
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||||
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{
|
||||||
Body: &ast.BlockStmt{
|
List: []ast.Stmt{
|
||||||
List: []ast.Stmt{
|
&ast.ReturnStmt{
|
||||||
&ast.ReturnStmt{
|
Results: []ast.Expr{
|
||||||
Results: []ast.Expr{
|
&ast.CallExpr{
|
||||||
&ast.CallExpr{
|
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
Args: []ast.Expr{
|
||||||
Args: []ast.Expr{
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||||
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)),
|
structField,
|
||||||
structField,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
})
|
}
|
||||||
}
|
}())
|
||||||
}
|
}
|
||||||
// final return nil
|
// final return nil
|
||||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("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 {
|
if col.Name == "created_at" && hasCreatedAt {
|
||||||
continue
|
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)
|
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{}}},
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// if item.ID == 0 {...} else {...}
|
|
||||||
ret = append(ret, &ast.IfStmt{
|
namedExecStmt := func(stmt string) []ast.Stmt {
|
||||||
Cond: &ast.BinaryExpr{
|
queryStmt := &ast.CallExpr{
|
||||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||||
Op: token.EQL,
|
Args: []ast.Expr{
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
&ast.BasicLit{Kind: token.STRING, Value: "`" + stmt + "`"},
|
||||||
},
|
ast.NewIdent(tbl.VarName),
|
||||||
Body: &ast.BlockStmt{
|
},
|
||||||
// Do create
|
}
|
||||||
List: append(
|
if !hasFks {
|
||||||
func() []ast.Stmt {
|
// No foreign key checking needed; just use `Must` for brevity
|
||||||
ret1 := []ast.Stmt{Comment("Do create")}
|
return []ast.Stmt{&ast.AssignStmt{
|
||||||
if hasCreatedAt {
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
// Auto-timestamps: created_at
|
Tok: token.DEFINE,
|
||||||
ret1 = append(ret1, &ast.AssignStmt{
|
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")}},
|
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{
|
|
||||||
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 hasFks {
|
||||||
// If there's foreign key checking, it needs to return an error (or nil)
|
// 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")}})
|
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"
|
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.
|
// 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 {
|
||||||
@@ -448,16 +599,15 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
return &ast.FuncDecl{
|
||||||
Recv: dbRecv,
|
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,
|
||||||
}
|
}
|
||||||
return funcDecl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateGetItemByIDFunc produces an AST for the `GetXyzByID()` function.
|
// GenerateGetItemByUniqColFunc 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 GenerateGetItemByUniqColFunc(tbl schema.Table, col schema.Column) *ast.FuncDecl {
|
func GenerateGetItemByUniqColFunc(tbl schema.Table, col schema.Column) *ast.FuncDecl {
|
||||||
// Use the xyzSQLFields constant in the select query
|
// Use the xyzSQLFields constant in the select query
|
||||||
@@ -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()),
|
Name: ast.NewIdent("Get" + schema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName()),
|
||||||
Type: &ast.FuncType{
|
Type: &ast.FuncType{
|
||||||
Params: &ast.FieldList{List: []*ast.Field{
|
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{
|
Results: &ast.FieldList{List: []*ast.Field{
|
||||||
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)},
|
{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.
|
// GenerateDeleteItemFunc produces an AST for the `DeleteXyz()` function.
|
||||||
// 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 {
|
||||||
arg := &ast.FieldList{List: []*ast.Field{{
|
colNames := []string{}
|
||||||
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
for _, c := range tbl.PrimaryKeyColumns() {
|
||||||
Type: ast.NewIdent(tbl.GoTypeName),
|
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{
|
funcBody := &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
@@ -566,41 +718,24 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
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{
|
Args: []ast.Expr{
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
|
&ast.BasicLit{Kind: token.STRING, Value: sqlStr},
|
||||||
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
ast.NewIdent(tbl.VarName),
|
||||||
},
|
},
|
||||||
})},
|
})},
|
||||||
},
|
},
|
||||||
&ast.IfStmt{
|
PanicIfRowsAffected(tbl),
|
||||||
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")},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
funcDecl := &ast.FuncDecl{
|
||||||
Recv: dbRecv,
|
Recv: dbRecv,
|
||||||
Name: ast.NewIdent("Delete" + tbl.GoTypeName),
|
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,
|
Body: funcBody,
|
||||||
}
|
}
|
||||||
return funcDecl
|
return funcDecl
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/jinzhu/inflection"
|
"github.com/jinzhu/inflection"
|
||||||
|
|
||||||
pkgschema "git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
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.
|
// 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"
|
packageName := "db"
|
||||||
testpackageName := packageName + "_test"
|
testpackageName := packageName + "_test"
|
||||||
|
|
||||||
|
makeHelperName := ast.NewIdent("Make" + tbl.GoTypeName)
|
||||||
|
|
||||||
// func MakeItem() Item { return Item{} }
|
// func MakeItem() Item { return Item{} }
|
||||||
makeItemFunc := &ast.FuncDecl{
|
makeItemFunc := &ast.FuncDecl{
|
||||||
Name: ast.NewIdent("Make" + tbl.GoTypeName),
|
Name: makeHelperName,
|
||||||
Type: &ast.FuncType{
|
Type: &ast.FuncType{
|
||||||
Params: &ast.FieldList{},
|
Params: &ast.FieldList{},
|
||||||
Results: &ast.FieldList{
|
Results: &ast.FieldList{
|
||||||
@@ -32,6 +35,24 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
Results: []ast.Expr{
|
Results: []ast.Expr{
|
||||||
&ast.CompositeLit{
|
&ast.CompositeLit{
|
||||||
Type: ast.NewIdent(tbl.GoTypeName),
|
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")
|
testObj := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName))
|
||||||
testObj2 := ast.NewIdent("item2")
|
testObj2 := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName) + "2")
|
||||||
fieldName := ast.NewIdent("Description")
|
fieldName := ast.NewIdent("Description") // TODO
|
||||||
description1 := `"an item"`
|
description1 := "an item"
|
||||||
description2 := `"a big item"`
|
description2 := "a big item"
|
||||||
testDB := ast.NewIdent("TestDB")
|
testDB := ast.NewIdent("TestDB")
|
||||||
|
|
||||||
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
|
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{
|
testFuncType := &ast.FuncType{
|
||||||
Params: &ast.FieldList{
|
Params: &ast.FieldList{
|
||||||
List: []*ast.Field{{
|
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{
|
testCreateUpdateDelete := &ast.FuncDecl{
|
||||||
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
|
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
|
||||||
Type: testFuncType,
|
Type: testFuncType,
|
||||||
@@ -72,26 +230,44 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
stmts := []ast.Stmt{
|
stmts := []ast.Stmt{
|
||||||
Comment("Create"),
|
Comment("Create"),
|
||||||
|
|
||||||
// item := Item{Description: "an item"}
|
// item := MakeItem()
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{testObj},
|
Lhs: []ast.Expr{testObj},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
Rhs: []ast.Expr{&ast.CompositeLit{
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: makeHelperName, Args: nil}},
|
||||||
Type: ast.NewIdent(tbl.GoTypeName),
|
},
|
||||||
Elts: []ast.Expr{
|
// item.Description = "an item"
|
||||||
&ast.KeyValueExpr{
|
&ast.AssignStmt{
|
||||||
Key: fieldName,
|
Lhs: []ast.Expr{
|
||||||
Value: &ast.BasicLit{Kind: token.STRING, Value: description1},
|
&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)
|
// TestDB.SaveItem(&item), possibly with error check
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&ast.ExprStmt{X: func() *ast.CallExpr {
|
||||||
Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent("Save" + tbl.GoTypeName)},
|
mainExpr := &ast.CallExpr{
|
||||||
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
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)
|
// require.NotZero(t, item.ID)
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&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)
|
// if deep.Equal(...) {...}
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
makeDeepEqual(testObj, testObj2),
|
||||||
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},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stmts = append(stmts,
|
stmts = append(stmts,
|
||||||
@@ -141,7 +310,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: fieldName}},
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: fieldName}},
|
||||||
Tok: token.ASSIGN,
|
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)
|
// TestDB.SaveItem(&item)
|
||||||
@@ -160,15 +329,8 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
})},
|
})},
|
||||||
},
|
},
|
||||||
|
|
||||||
// assert.Equal(t, item.Description, item2.Description)
|
// if deep.Equal(...) {...}
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
makeDeepEqual(testObj, testObj2),
|
||||||
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},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
indexGets, hasIndexedGets := []ast.Stmt{
|
indexGets, hasIndexedGets := []ast.Stmt{
|
||||||
@@ -191,7 +353,9 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
ast.NewIdent("t"),
|
ast.NewIdent("t"),
|
||||||
testObj2,
|
testObj2,
|
||||||
mustCall(&ast.CallExpr{
|
mustCall(&ast.CallExpr{
|
||||||
Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent("Get" + pkgschema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName())},
|
Fun: &ast.SelectorExpr{X: testDB, Sel: ast.NewIdent(
|
||||||
|
"Get" + pkgschema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName(),
|
||||||
|
)},
|
||||||
Args: []ast.Expr{&ast.SelectorExpr{X: testObj2, Sel: ast.NewIdent(col.GoFieldName())}},
|
Args: []ast.Expr{&ast.SelectorExpr{X: testObj2, Sel: ast.NewIdent(col.GoFieldName())}},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
@@ -261,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{
|
testList := []ast.Decl{
|
||||||
makeItemFunc,
|
makeItemFunc,
|
||||||
testCreateUpdateDelete,
|
testCreateUpdateDelete,
|
||||||
@@ -374,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)},
|
Path: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf(`"%s/pkg/%s"`, gomodName, packageName)},
|
||||||
Name: ast.NewIdent("."),
|
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/assert"`}},
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"github.com/stretchr/testify/require"`}},
|
&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"
|
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, just lowercase the field name
|
// Otherwise, just use the whole name
|
||||||
fieldname := c.GoFieldName()
|
return c.LongGoVarName()
|
||||||
return strings.ToLower(fieldname)[0:1] + fieldname[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c Column) GoTypeName() string {
|
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
||||||
if c.IsNonCodeTableForeignKey() {
|
func (c Column) LongGoVarName() string {
|
||||||
return TypenameFromTablename(c.ForeignKeyTargetTable) + "ID"
|
return textutils.CamelToPascal(c.GoFieldName())
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table is a single SQLite table.
|
// Table is a single SQLite table.
|
||||||
|
|||||||
@@ -9,3 +9,7 @@ func SnakeToCamel(s string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(parts, "")
|
return strings.Join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func CamelToPascal(s string) string {
|
||||||
|
return strings.ToLower(s)[0:1] + s[1:]
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user