Compare commits
10 Commits
fcf266eb1d
...
wispem/cod
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e37a0e07 | |||
| f8664ed514 | |||
| 8c29d455ff | |||
| 0f9a57dd85 | |||
| 11fed4b9c7 | |||
| 4e0836eb2e | |||
| f82929f6e2 | |||
| 1bc7f9111f | |||
| c1150954e5 | |||
| fd90830340 |
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
|
||||||
@@ -22,6 +22,7 @@ func main() {
|
|||||||
root_cmd.AddCommand(sqlite_lint)
|
root_cmd.AddCommand(sqlite_lint)
|
||||||
root_cmd.AddCommand(cmd_init)
|
root_cmd.AddCommand(cmd_init)
|
||||||
root_cmd.AddCommand(generate_model)
|
root_cmd.AddCommand(generate_model)
|
||||||
|
root_cmd.AddCommand(generate_codetable_type)
|
||||||
if err := root_cmd.Execute(); err != nil {
|
if err := root_cmd.Execute(); err != nil {
|
||||||
fmt.Println(RED + err.Error() + RESET)
|
fmt.Println(RED + err.Error() + RESET)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
55
cmd/subcmd_generate_codetable_type.go
Normal file
55
cmd/subcmd_generate_codetable_type.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/codegen/modelgenerate"
|
||||||
|
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
var generate_codetable_type = &cobra.Command{
|
||||||
|
Use: "generate_codetable <table_name>",
|
||||||
|
Short: "Generate a code-table enum type",
|
||||||
|
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
path := Must(cmd.Flags().GetString("schema"))
|
||||||
|
sql, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reading path %s: %w", path, err)
|
||||||
|
}
|
||||||
|
db := schema.InitDB(string(sql))
|
||||||
|
schema := schema.SchemaFromDB(db)
|
||||||
|
table, isOk := schema.Tables[args[0]]
|
||||||
|
if !isOk {
|
||||||
|
return ErrNoSuchTable
|
||||||
|
}
|
||||||
|
|
||||||
|
vals := table.GetCodeTableValues(db)
|
||||||
|
decls := []ast.Decl{
|
||||||
|
modelgenerate.GenerateCodetableType(table),
|
||||||
|
modelgenerate.GenerateCodetableEnum(table, vals),
|
||||||
|
modelgenerate.GenerateCodetableStringerFunc(table, vals),
|
||||||
|
}
|
||||||
|
|
||||||
|
file := &ast.File{
|
||||||
|
Name: ast.NewIdent("db"), // TODO: parameterize
|
||||||
|
Decls: decls,
|
||||||
|
}
|
||||||
|
|
||||||
|
PanicIf(modelgenerate.FprintWithComments(os.Stdout, file))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// DUPE: generate-flags
|
||||||
|
func init() {
|
||||||
|
generate_codetable_type.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||||
|
generate_codetable_type.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||||
|
generate_codetable_type.Flags().Bool("test", false, "Generate test file instead of regular file")
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -94,6 +101,7 @@ var generate_model = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DUPE: generate-flags
|
||||||
func init() {
|
func init() {
|
||||||
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||||
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
111
pkg/codegen/modelgenerate/generate_codetable_type.go
Normal file
111
pkg/codegen/modelgenerate/generate_codetable_type.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package modelgenerate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateCodetableType(table schema.Table) *ast.GenDecl {
|
||||||
|
return &ast.GenDecl{
|
||||||
|
Tok: token.TYPE,
|
||||||
|
Specs: []ast.Spec{&ast.TypeSpec{Name: &ast.Ident{Name: table.GoTypeName}, Type: &ast.Ident{Name: "int"}}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateCodetableEnum(table schema.Table, vals []string) *ast.GenDecl {
|
||||||
|
getConstName := func(s string) string {
|
||||||
|
return table.GoTypeName + textutils.KebabToPascal(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
constSpecs := []ast.Spec{}
|
||||||
|
for i, val := range vals {
|
||||||
|
spec := &ast.ValueSpec{
|
||||||
|
Names: []*ast.Ident{{Name: getConstName(val)}},
|
||||||
|
}
|
||||||
|
// Only the first one needs `iota`
|
||||||
|
if i == 0 {
|
||||||
|
spec.Type = &ast.Ident{Name: table.GoTypeName}
|
||||||
|
spec.Values = []ast.Expr{&ast.BinaryExpr{
|
||||||
|
X: &ast.Ident{Name: "iota"},
|
||||||
|
Op: token.ADD,
|
||||||
|
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
constSpecs = append(constSpecs, spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ast.GenDecl{Tok: token.CONST, Specs: constSpecs}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateCodetableStringerFunc implements the `Stringer` interface by defining a `String() string` function.
|
||||||
|
func GenerateCodetableStringerFunc(table schema.Table, vals []string) *ast.FuncDecl {
|
||||||
|
objIdent := ast.NewIdent(table.VarName)
|
||||||
|
return &ast.FuncDecl{
|
||||||
|
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{objIdent}, Type: &ast.Ident{Name: table.GoTypeName}}}},
|
||||||
|
Name: &ast.Ident{Name: "String"},
|
||||||
|
Type: &ast.FuncType{Params: &ast.FieldList{}, Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "string"}}}}},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
// names := []string{ ... }
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{&ast.Ident{Name: "names"}},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{
|
||||||
|
&ast.CompositeLit{
|
||||||
|
Type: &ast.ArrayType{Elt: &ast.Ident{Name: "string"}},
|
||||||
|
Elts: func() []ast.Expr {
|
||||||
|
ret := []ast.Expr{
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: `""`},
|
||||||
|
}
|
||||||
|
for _, val := range vals {
|
||||||
|
ret = append(ret, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", val)})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// if int(c) < 1 || int(c) >= len(names) { return "invalid" }
|
||||||
|
&ast.IfStmt{
|
||||||
|
Cond: &ast.BinaryExpr{
|
||||||
|
X: &ast.BinaryExpr{
|
||||||
|
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
||||||
|
Op: token.LSS,
|
||||||
|
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||||
|
},
|
||||||
|
Op: token.LOR,
|
||||||
|
Y: &ast.BinaryExpr{
|
||||||
|
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
||||||
|
Op: token.GEQ,
|
||||||
|
Y: &ast.CallExpr{Fun: &ast.Ident{Name: "len"}, Args: []ast.Expr{&ast.Ident{Name: "names"}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
&ast.ReturnStmt{Results: []ast.Expr{
|
||||||
|
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Sprintf")}, Args: []ast.Expr{
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: `"<%d=invalid>"`},
|
||||||
|
objIdent,
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// return names[c]
|
||||||
|
&ast.ReturnStmt{
|
||||||
|
Results: []ast.Expr{
|
||||||
|
&ast.IndexExpr{
|
||||||
|
X: &ast.Ident{Name: "names"},
|
||||||
|
Index: objIdent,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
// ---------------
|
// ---------------
|
||||||
@@ -255,10 +270,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 +310,132 @@ 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{
|
|
||||||
Cond: &ast.BinaryExpr{
|
|
||||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
|
||||||
Op: token.EQL,
|
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
|
||||||
},
|
|
||||||
Body: &ast.BlockStmt{
|
|
||||||
// Do create
|
|
||||||
List: append(
|
|
||||||
func() []ast.Stmt {
|
|
||||||
ret1 := []ast.Stmt{Comment("Do create")}
|
|
||||||
if hasCreatedAt {
|
|
||||||
// Auto-timestamps: created_at
|
|
||||||
ret1 = append(ret1, &ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
|
|
||||||
Tok: token.ASSIGN,
|
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
namedExecStmt := &ast.CallExpr{
|
|
||||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
|
||||||
Args: []ast.Expr{
|
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
|
||||||
ast.NewIdent(tbl.VarName),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if !hasFks {
|
|
||||||
// No foreign key checking needed; just use `Must` for brevity
|
|
||||||
return append(ret1, &ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
|
||||||
Tok: token.DEFINE,
|
|
||||||
Rhs: []ast.Expr{mustCall(namedExecStmt)},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(ret1,
|
namedExecStmt := func(stmt string) []ast.Stmt {
|
||||||
// result, err := db.DB.NamedExec(`...`, u)
|
queryStmt := &ast.CallExpr{
|
||||||
&ast.AssignStmt{
|
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||||
Lhs: []ast.Expr{
|
Args: []ast.Expr{
|
||||||
ast.NewIdent("result"),
|
&ast.BasicLit{Kind: token.STRING, Value: "`" + stmt + "`"},
|
||||||
ast.NewIdent("err"),
|
ast.NewIdent(tbl.VarName),
|
||||||
},
|
|
||||||
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")}}}}}}}},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
})
|
if !hasFks {
|
||||||
|
// No foreign key checking needed; just use `Must` for brevity
|
||||||
|
return []ast.Stmt{&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{mustCall(queryStmt)},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
// There's foreign keys
|
||||||
|
return []ast.Stmt{
|
||||||
|
// result, err := db.DB.NamedExec(`...`, u)
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{
|
||||||
|
ast.NewIdent("result"),
|
||||||
|
ast.NewIdent("err"),
|
||||||
|
},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{queryStmt},
|
||||||
|
},
|
||||||
|
// if fkErr := checkForeignKeyFailures(err); fkErr != nil { return fkErr } else if err != nil { panic(err) }
|
||||||
|
&ast.IfStmt{
|
||||||
|
Init: &ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("fkErr")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{
|
||||||
|
&ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("checkForeignKeyFailures"),
|
||||||
|
Args: []ast.Expr{ast.NewIdent("err")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cond: &ast.BinaryExpr{
|
||||||
|
X: ast.NewIdent("fkErr"),
|
||||||
|
Op: token.NEQ,
|
||||||
|
Y: ast.NewIdent("nil"),
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
&ast.ReturnStmt{
|
||||||
|
Results: []ast.Expr{ast.NewIdent("fkErr")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Else: func() *ast.IfStmt {
|
||||||
|
panicStmt := &ast.ExprStmt{
|
||||||
|
X: &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("panic"),
|
||||||
|
Args: []ast.Expr{ast.NewIdent("err")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
TrailingComments[panicStmt] = "not a foreign key error"
|
||||||
|
return &ast.IfStmt{
|
||||||
|
Cond: &ast.BinaryExpr{
|
||||||
|
X: ast.NewIdent("err"),
|
||||||
|
Op: token.NEQ,
|
||||||
|
Y: ast.NewIdent("nil"),
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{panicStmt},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if tbl.IsWithoutRowid {
|
||||||
|
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.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 +469,61 @@ 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 = :%s", col.Name, 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 +556,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 +662,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 +675,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,8 +60,8 @@ 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"`
|
||||||
@@ -94,13 +97,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,7 +111,7 @@ 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),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -148,12 +151,12 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
})},
|
})},
|
||||||
},
|
},
|
||||||
|
|
||||||
// assert.Equal(t, "an item", item2.Description)
|
// assert.Equal(t, item.Description, item2.Description)
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
||||||
Args: []ast.Expr{
|
Args: []ast.Expr{
|
||||||
ast.NewIdent("t"),
|
ast.NewIdent("t"),
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: description1},
|
&ast.SelectorExpr{X: testObj, Sel: fieldName},
|
||||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
@@ -307,11 +310,11 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
shouldDefineErr := true
|
||||||
for _, col := range tbl.Columns {
|
for _, col := range tbl.Columns {
|
||||||
if col.IsForeignKey {
|
if col.IsForeignKey {
|
||||||
shouldIncludeTestFkCheck = true
|
shouldIncludeTestFkCheck = true
|
||||||
stmts = append(stmts, []ast.Stmt{
|
stmts = append(stmts, []ast.Stmt{
|
||||||
|
|
||||||
// post.QuotedPostID = 94354538969386985
|
// post.QuotedPostID = 94354538969386985
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{
|
Lhs: []ast.Expr{
|
||||||
@@ -332,7 +335,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
// err := db.SavePost(&post)
|
// err := db.SavePost(&post)
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||||
Tok: token.DEFINE,
|
Tok: map[bool]token.Token{true: token.DEFINE, false: token.ASSIGN}[shouldDefineErr],
|
||||||
Rhs: []ast.Expr{
|
Rhs: []ast.Expr{
|
||||||
&ast.CallExpr{
|
&ast.CallExpr{
|
||||||
Fun: &ast.SelectorExpr{
|
Fun: &ast.SelectorExpr{
|
||||||
@@ -368,6 +371,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}...)
|
}...)
|
||||||
|
shouldDefineErr = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return stmts
|
return stmts
|
||||||
|
|||||||
67
pkg/schema/column.go
Normal file
67
pkg/schema/column.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Column represents a single column in a table.
|
||||||
|
type Column struct {
|
||||||
|
// TableName is the name of the SQLite table this column belongs to.
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
|
||||||
|
// Name is the SQLite column name.
|
||||||
|
Name string `db:"column_name"`
|
||||||
|
|
||||||
|
// Type is the SQLite type this column contains.
|
||||||
|
Type string `db:"column_type"`
|
||||||
|
|
||||||
|
IsNotNull bool `db:"notnull"`
|
||||||
|
HasDefaultValue bool `db:"has_default_value"`
|
||||||
|
DefaultValue string `db:"dflt_value"`
|
||||||
|
IsPrimaryKey bool `db:"is_primary_key"`
|
||||||
|
PrimaryKeyRank uint `db:"primary_key_rank"`
|
||||||
|
IsForeignKey bool `db:"is_foreign_key"`
|
||||||
|
ForeignKeyTargetTable string `db:"fk_target_table"`
|
||||||
|
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNullableForeignKey is a helper function.
|
||||||
|
func (c Column) IsNullableForeignKey() bool {
|
||||||
|
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) IsNonCodeTableForeignKey() bool {
|
||||||
|
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) GoFieldName() string {
|
||||||
|
if c.Name == "rowid" {
|
||||||
|
return "ID"
|
||||||
|
}
|
||||||
|
if c.IsNonCodeTableForeignKey() {
|
||||||
|
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
||||||
|
}
|
||||||
|
return textutils.SnakeToCamel(c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
||||||
|
func (c Column) GoVarName() string {
|
||||||
|
if c.Name == "rowid" {
|
||||||
|
return strings.ToLower(c.TableName)[0:1] + "ID"
|
||||||
|
// TODO: Or should it just be "id"??
|
||||||
|
}
|
||||||
|
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
||||||
|
if c.IsNonCodeTableForeignKey() {
|
||||||
|
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, just use the whole name
|
||||||
|
return c.LongGoVarName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
||||||
|
func (c Column) LongGoVarName() string {
|
||||||
|
return textutils.CamelToPascal(c.GoFieldName())
|
||||||
|
}
|
||||||
10
pkg/schema/index.go
Normal file
10
pkg/schema/index.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
type Index struct {
|
||||||
|
Name string `db:"index_name"`
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
Columns []string
|
||||||
|
IsUnique bool `db:"is_unique"`
|
||||||
|
// TODO: `where ...` for partial indexes
|
||||||
|
// TODO: identify columns that are expressions
|
||||||
|
}
|
||||||
6
pkg/schema/schema.go
Normal file
6
pkg/schema/schema.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Tables map[string]Table
|
||||||
|
Indexes map[string]Index
|
||||||
|
}
|
||||||
@@ -1,62 +1,14 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Column represents a single column in a table.
|
|
||||||
type Column struct {
|
|
||||||
TableName string `db:"table_name"`
|
|
||||||
Name string `db:"column_name"`
|
|
||||||
Type string `db:"column_type"`
|
|
||||||
IsNotNull bool `db:"notnull"`
|
|
||||||
HasDefaultValue bool `db:"has_default_value"`
|
|
||||||
DefaultValue string `db:"dflt_value"`
|
|
||||||
IsPrimaryKey bool `db:"is_primary_key"`
|
|
||||||
PrimaryKeyRank uint `db:"primary_key_rank"`
|
|
||||||
IsForeignKey bool `db:"is_foreign_key"`
|
|
||||||
ForeignKeyTargetTable string `db:"fk_target_table"`
|
|
||||||
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNullableForeignKey is a helper function.
|
|
||||||
func (c Column) IsNullableForeignKey() bool {
|
|
||||||
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Column) IsNonCodeTableForeignKey() bool {
|
|
||||||
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Column) GoFieldName() string {
|
|
||||||
if c.Name == "rowid" {
|
|
||||||
return "ID"
|
|
||||||
}
|
|
||||||
if c.IsNonCodeTableForeignKey() {
|
|
||||||
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
|
||||||
}
|
|
||||||
return textutils.SnakeToCamel(c.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
|
||||||
func (c Column) GoVarName() string {
|
|
||||||
if c.Name == "rowid" {
|
|
||||||
return strings.ToLower(c.TableName)[0:1] + "ID"
|
|
||||||
// TODO: Or should it just be "id"??
|
|
||||||
}
|
|
||||||
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
|
||||||
if c.IsNonCodeTableForeignKey() {
|
|
||||||
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, just lowercase the field name
|
|
||||||
fieldname := c.GoFieldName()
|
|
||||||
return strings.ToLower(fieldname)[0:1] + fieldname[1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Table is a single SQLite table.
|
// Table is a single SQLite table.
|
||||||
type Table struct {
|
type Table struct {
|
||||||
TableName string `db:"name"`
|
TableName string `db:"name"`
|
||||||
@@ -115,16 +67,10 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type Index struct {
|
func (t Table) GetCodeTableValues(db *sqlx.DB) (ret []string) {
|
||||||
Name string `db:"index_name"`
|
if !slices.ContainsFunc(t.Columns, func(c Column) bool { return c.Name == "name" }) {
|
||||||
TableName string `db:"table_name"`
|
panic("not a code table")
|
||||||
Columns []string
|
}
|
||||||
IsUnique bool `db:"is_unique"`
|
flowutils.PanicIf(db.Select(&ret, fmt.Sprintf("select name from %s", t.TableName)))
|
||||||
// TODO: `where ...` for partial indexes
|
return
|
||||||
// TODO: identify columns that are expressions
|
|
||||||
}
|
|
||||||
|
|
||||||
type Schema struct {
|
|
||||||
Tables map[string]Table
|
|
||||||
Indexes map[string]Index
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,3 +9,17 @@ func SnakeToCamel(s string) string {
|
|||||||
}
|
}
|
||||||
return strings.Join(parts, "")
|
return strings.Join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func KebabToPascal(s string) string {
|
||||||
|
parts := strings.Split(s, "-")
|
||||||
|
for i, part := range parts {
|
||||||
|
if len(part) > 0 {
|
||||||
|
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CamelToPascal(s string) string {
|
||||||
|
return strings.ToLower(s)[0:1] + s[1:]
|
||||||
|
}
|
||||||
|
|||||||
8
sample_data/test_schemas/codetables.sql
Normal file
8
sample_data/test_schemas/codetables.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
create table item_types (
|
||||||
|
rowid integer primary key,
|
||||||
|
name text not null unique
|
||||||
|
) strict;
|
||||||
|
insert into item_types(rowid, name) values
|
||||||
|
(1, 'first-type'),
|
||||||
|
(2, 'second-type'),
|
||||||
|
(3, 'third-type');
|
||||||
Reference in New Issue
Block a user