Compare commits
18 Commits
fcf266eb1d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 616304c7dd | |||
| 17fc8a68f6 | |||
| d572745613 | |||
| 9bfb31798c | |||
| eafeb658bd | |||
| dbf14e23b6 | |||
| ad1782c73d | |||
| ccd7e32cbf | |||
| ed4ade1956 | |||
| 9a11f3986c | |||
| 8c29d455ff | |||
| 0f9a57dd85 | |||
| 11fed4b9c7 | |||
| 4e0836eb2e | |||
| f82929f6e2 | |||
| 1bc7f9111f | |||
| c1150954e5 | |||
| fd90830340 |
@@ -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
|
||||||
|
|||||||
@@ -46,11 +46,19 @@ create table items (
|
|||||||
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
|
||||||
|
|||||||
@@ -18,9 +18,8 @@ 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 {
|
||||||
@@ -54,6 +53,22 @@ func GoTypeForColumn(c schema.Column) ast.Expr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
// ---------------
|
// ---------------
|
||||||
@@ -146,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.BasicLit{Kind: token.STRING, Value: 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")}})
|
||||||
@@ -255,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)
|
||||||
|
|
||||||
@@ -276,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")}})
|
||||||
@@ -442,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 {
|
||||||
@@ -474,13 +599,12 @@ 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateGetItemByUniqColFunc produces an AST for the `GetXyzByID()` function.
|
// GenerateGetItemByUniqColFunc produces an AST for the `GetXyzByID()` function.
|
||||||
@@ -581,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{
|
||||||
@@ -592,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{
|
||||||
@@ -57,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") // TODO
|
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{{
|
||||||
@@ -75,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,
|
||||||
@@ -94,13 +234,13 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{testObj},
|
Lhs: []ast.Expr{testObj},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("MakeItem"), Args: nil}},
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: makeHelperName, Args: nil}},
|
||||||
},
|
},
|
||||||
// item.Description = "an item"
|
// item.Description = "an item"
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{
|
Lhs: []ast.Expr{
|
||||||
&ast.SelectorExpr{
|
&ast.SelectorExpr{
|
||||||
X: ast.NewIdent("item"),
|
X: testObj,
|
||||||
Sel: ast.NewIdent("Description"),
|
Sel: ast.NewIdent("Description"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -108,16 +248,26 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
Rhs: []ast.Expr{
|
Rhs: []ast.Expr{
|
||||||
&ast.BasicLit{
|
&ast.BasicLit{
|
||||||
Kind: token.STRING,
|
Kind: token.STRING,
|
||||||
Value: `"an item"`,
|
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{
|
||||||
@@ -148,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,
|
||||||
@@ -167,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)
|
||||||
@@ -186,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{
|
||||||
@@ -289,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,
|
||||||
@@ -402,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,9 +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:]
|
}
|
||||||
|
|
||||||
|
// 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.
|
// 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