Compare commits
12 Commits
e53546a7f5
...
wispem/cod
| Author | SHA1 | Date | |
|---|---|---|---|
| 01e37a0e07 | |||
| f8664ed514 | |||
| 8c29d455ff | |||
| 0f9a57dd85 | |||
| 11fed4b9c7 | |||
| 4e0836eb2e | |||
| f82929f6e2 | |||
| 1bc7f9111f | |||
| c1150954e5 | |||
| fd90830340 | |||
| fcf266eb1d | |||
| 29787b5521 |
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(cmd_init)
|
||||
root_cmd.AddCommand(generate_model)
|
||||
root_cmd.AddCommand(generate_codetable_type)
|
||||
if err := root_cmd.Execute(); err != nil {
|
||||
fmt.Println(RED + err.Error() + RESET)
|
||||
os.Exit(1)
|
||||
|
||||
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{
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"database/sql"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"errors"`}},
|
||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
||||
&ast.ImportSpec{
|
||||
Name: ast.NewIdent("."),
|
||||
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"`},
|
||||
@@ -69,8 +68,16 @@ var generate_model = &cobra.Command{
|
||||
modelgenerate.GenerateSQLFieldsConst(table),
|
||||
modelgenerate.GenerateSaveItemFunc(table),
|
||||
modelgenerate.GenerateDeleteItemFunc(table),
|
||||
)
|
||||
if table.IsWithoutRowid {
|
||||
decls = append(decls,
|
||||
modelgenerate.GenerateGetItemBy(table, table.PrimaryKeyColumns()),
|
||||
)
|
||||
} else {
|
||||
decls = append(decls,
|
||||
modelgenerate.GenerateGetItemByIDFunc(table),
|
||||
)
|
||||
}
|
||||
for _, index := range schema.Indexes {
|
||||
if index.TableName != table.TableName {
|
||||
// Skip indexes on other tables
|
||||
@@ -94,6 +101,7 @@ var generate_model = &cobra.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// DUPE: generate-flags
|
||||
func init() {
|
||||
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||
|
||||
@@ -41,15 +41,24 @@ create table items (
|
||||
rowid integer primary key,
|
||||
description text not null default '',
|
||||
flavor integer references item_flavor(rowid),
|
||||
data blob not null,
|
||||
thing text not null unique,
|
||||
created_at integer not null,
|
||||
updated_at integer not null
|
||||
) strict;
|
||||
|
||||
create table item_to_item (
|
||||
item1_id integer references items(rowid),
|
||||
item2_id integer references items(rowid),
|
||||
primary key (item1_id, item2_id)
|
||||
) strict, without rowid;
|
||||
|
||||
EOF
|
||||
|
||||
# Generate an item model and test file
|
||||
$gas generate items > pkg/db/item.go
|
||||
$gas generate items --test > pkg/db/item_test.go
|
||||
$gas generate item_to_item > pkg/db/item_to_item.go
|
||||
go mod tidy
|
||||
|
||||
# Run the tests
|
||||
|
||||
@@ -76,7 +76,12 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
||||
return fmt.Errorf("re-parsing pretty-print: %w", err)
|
||||
}
|
||||
|
||||
// Convert the tree-of-nodes into a slice-of-nodes
|
||||
// Parallel walk: apply TrailingComments from the side map.
|
||||
// Both trees have identical structure (the reparse is just a positioned copy),
|
||||
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
||||
// avoid mismatches from Doc fields.
|
||||
if len(TrailingComments) > 0 {
|
||||
// Helper: convert the tree-of-nodes into a slice-of-nodes
|
||||
collectNodes := func(node ast.Node) []ast.Node {
|
||||
var nodes []ast.Node
|
||||
ast.Inspect(node, func(n ast.Node) bool {
|
||||
@@ -91,13 +96,15 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
|
||||
return nodes
|
||||
}
|
||||
|
||||
// Parallel walk: apply TrailingComments from the side map.
|
||||
// Both trees have identical structure (the reparse is just a positioned copy),
|
||||
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
||||
// avoid mismatches from Doc fields.
|
||||
if len(TrailingComments) > 0 {
|
||||
origNodes := collectNodes(file)
|
||||
reparsedNodes := collectNodes(parsed)
|
||||
if len(origNodes) != len(reparsedNodes) {
|
||||
panic(fmt.Sprintf(
|
||||
"origNodes: %d; reparsedNodes: %d. The AST generator is likely generating an invalid AST",
|
||||
len(origNodes), len(reparsedNodes),
|
||||
))
|
||||
}
|
||||
|
||||
for i, orig := range origNodes {
|
||||
text, isOk := TrailingComments[orig]
|
||||
if !isOk {
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,55 @@ import (
|
||||
var (
|
||||
dbRecv = &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
|
||||
dbDB = &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("DB")}
|
||||
fmtErrorf = &ast.SelectorExpr{X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Errorf")}
|
||||
)
|
||||
|
||||
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
||||
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
|
||||
}
|
||||
|
||||
// GoTypeForColumn returns a type expression for this column.
|
||||
//
|
||||
// For most columns this isjust its mapped name as a `ast.NewIdent`, but for "blob" it needs
|
||||
// a slice expression (`[]byte`).
|
||||
func GoTypeForColumn(c schema.Column) ast.Expr {
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return ast.NewIdent(schema.TypenameFromTablename(c.ForeignKeyTargetTable) + "ID")
|
||||
}
|
||||
switch c.Type {
|
||||
case "integer", "int":
|
||||
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
|
||||
return ast.NewIdent("bool")
|
||||
} else if strings.HasSuffix(c.Name, "_at") {
|
||||
return ast.NewIdent("Timestamp")
|
||||
}
|
||||
return ast.NewIdent("int")
|
||||
case "text":
|
||||
return ast.NewIdent("string")
|
||||
case "real":
|
||||
return ast.NewIdent("float32")
|
||||
case "blob":
|
||||
return &ast.ArrayType{Elt: ast.NewIdent("byte")}
|
||||
default:
|
||||
panic("Unrecognized sqlite column type: " + c.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func PanicIfRowsAffected(tbl schema.Table) *ast.IfStmt {
|
||||
return &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
|
||||
Args: []ast.Expr{},
|
||||
}),
|
||||
Op: token.NEQ,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||
},
|
||||
Body: &ast.BlockStmt{List: []ast.Stmt{
|
||||
&ast.ExprStmt{X: &ast.CallExpr{Fun: ast.NewIdent("panic"), Args: []ast.Expr{ast.NewIdent(tbl.VarName)}}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------
|
||||
// Generators
|
||||
// ---------------
|
||||
@@ -63,10 +105,9 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||
})
|
||||
} else {
|
||||
typeName := col.GoTypeName()
|
||||
fields = append(fields, &ast.Field{
|
||||
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))},
|
||||
Type: ast.NewIdent(typeName),
|
||||
Type: GoTypeForColumn(col),
|
||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||
})
|
||||
}
|
||||
@@ -183,7 +224,7 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)),
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||
structField,
|
||||
},
|
||||
},
|
||||
@@ -229,10 +270,29 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
if col.Name == "created_at" && hasCreatedAt {
|
||||
continue
|
||||
}
|
||||
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 "))
|
||||
}
|
||||
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 "))
|
||||
}
|
||||
|
||||
checkForeignKeyFailuresAssignment, hasFks := buildFKCheckLambda(tbl)
|
||||
|
||||
@@ -250,43 +310,25 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||
})
|
||||
}
|
||||
// if item.ID == 0 {...} else {...}
|
||||
ret = append(ret, &ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
Op: token.EQL,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
||||
},
|
||||
Body: &ast.BlockStmt{
|
||||
// Do create
|
||||
List: append(
|
||||
func() []ast.Stmt {
|
||||
ret1 := []ast.Stmt{Comment("Do create")}
|
||||
if hasCreatedAt {
|
||||
// Auto-timestamps: created_at
|
||||
ret1 = append(ret1, &ast.AssignStmt{
|
||||
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{
|
||||
|
||||
namedExecStmt := func(stmt string) []ast.Stmt {
|
||||
queryStmt := &ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + stmt + "`"},
|
||||
ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
}
|
||||
if !hasFks {
|
||||
// No foreign key checking needed; just use `Must` for brevity
|
||||
return append(ret1, &ast.AssignStmt{
|
||||
return []ast.Stmt{&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{mustCall(namedExecStmt)},
|
||||
})
|
||||
Rhs: []ast.Expr{mustCall(queryStmt)},
|
||||
}}
|
||||
}
|
||||
|
||||
return append(ret1,
|
||||
// There's foreign keys
|
||||
return []ast.Stmt{
|
||||
// result, err := db.DB.NamedExec(`...`, u)
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
@@ -294,9 +336,8 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
ast.NewIdent("err"),
|
||||
},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{namedExecStmt},
|
||||
Rhs: []ast.Expr{queryStmt},
|
||||
},
|
||||
|
||||
// if fkErr := checkForeignKeyFailures(err); fkErr != nil { return fkErr } else if err != nil { panic(err) }
|
||||
&ast.IfStmt{
|
||||
Init: &ast.AssignStmt{
|
||||
@@ -341,7 +382,34 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
}
|
||||
}(),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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")}},
|
||||
@@ -358,31 +426,16 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
},
|
||||
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")}}}}}}}},
|
||||
},
|
||||
},
|
||||
List: append(
|
||||
[]ast.Stmt{Comment("Do update")},
|
||||
append(
|
||||
namedExecStmt(updateStmt),
|
||||
PanicIfRowsAffected(tbl),
|
||||
)...,
|
||||
),
|
||||
},
|
||||
})
|
||||
}
|
||||
if hasFks {
|
||||
// If there's foreign key checking, it needs to return an error (or nil)
|
||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
|
||||
@@ -416,6 +469,61 @@ func getByIDFuncName(tblname string) string {
|
||||
return "Get" + schema.TypenameFromTablename(tblname) + "ByID"
|
||||
}
|
||||
|
||||
func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
||||
colNames := []string{}
|
||||
funcNameSuffix := []string{}
|
||||
funcParams := &ast.FieldList{List: []*ast.Field{}}
|
||||
sqlParams := []ast.Expr{}
|
||||
for _, col := range cols {
|
||||
funcParam := ast.NewIdent(col.LongGoVarName())
|
||||
funcParams.List = append(funcParams.List, &ast.Field{Names: []*ast.Ident{funcParam}, Type: GoTypeForColumn(col)})
|
||||
colNames = append(colNames, fmt.Sprintf("%s = :%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.
|
||||
// E.g., a table with `table.TypeName = "foods"` will produce a "GetFoodByID()" function.
|
||||
func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
@@ -448,13 +556,12 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
},
|
||||
}
|
||||
|
||||
funcDecl := &ast.FuncDecl{
|
||||
return &ast.FuncDecl{
|
||||
Recv: dbRecv,
|
||||
Name: ast.NewIdent(getByIDFuncName(tbl.TableName)),
|
||||
Type: &ast.FuncType{Params: arg, Results: result},
|
||||
Body: funcBody,
|
||||
}
|
||||
return funcDecl
|
||||
}
|
||||
|
||||
// GenerateGetItemByUniqColFunc produces an AST for the `GetXyzByID()` function.
|
||||
@@ -478,7 +585,7 @@ func GenerateGetItemByUniqColFunc(tbl schema.Table, col schema.Column) *ast.Func
|
||||
Name: ast.NewIdent("Get" + schema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName()),
|
||||
Type: &ast.FuncType{
|
||||
Params: &ast.FieldList{List: []*ast.Field{
|
||||
{Names: []*ast.Ident{param}, Type: ast.NewIdent(col.GoTypeName())},
|
||||
{Names: []*ast.Ident{param}, Type: GoTypeForColumn(col)},
|
||||
}},
|
||||
Results: &ast.FieldList{List: []*ast.Field{
|
||||
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)},
|
||||
@@ -555,10 +662,12 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
// GenerateDeleteItemFunc produces an AST for the `DeleteXyz()` function.
|
||||
// E.g., a table with `table.TypeName = "foods"` will produce a "DeleteFood()" function.
|
||||
func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
arg := &ast.FieldList{List: []*ast.Field{{
|
||||
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
}}}
|
||||
colNames := []string{}
|
||||
for _, c := range tbl.PrimaryKeyColumns() {
|
||||
colNames = append(colNames, fmt.Sprintf("%s = :%s", c.Name, c.Name))
|
||||
}
|
||||
|
||||
sqlStr := "`delete from " + tbl.TableName + fmt.Sprintf(" where %s`", strings.Join(colNames, " and "))
|
||||
|
||||
funcBody := &ast.BlockStmt{
|
||||
List: []ast.Stmt{
|
||||
@@ -566,41 +675,24 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Exec")},
|
||||
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
|
||||
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
&ast.BasicLit{Kind: token.STRING, Value: sqlStr},
|
||||
ast.NewIdent(tbl.VarName),
|
||||
},
|
||||
})},
|
||||
},
|
||||
&ast.IfStmt{
|
||||
Cond: &ast.BinaryExpr{
|
||||
X: mustCall(
|
||||
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}},
|
||||
),
|
||||
Op: token.NEQ,
|
||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||
},
|
||||
Body: &ast.BlockStmt{List: []ast.Stmt{
|
||||
&ast.ExprStmt{X: &ast.CallExpr{
|
||||
Fun: ast.NewIdent("panic"),
|
||||
Args: []ast.Expr{&ast.CallExpr{
|
||||
Fun: fmtErrorf,
|
||||
Args: []ast.Expr{
|
||||
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("\"tried to delete %s with ID (%%d) but it doesn't exist\"", strings.ToLower(tbl.GoTypeName))},
|
||||
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||
},
|
||||
}},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
PanicIfRowsAffected(tbl),
|
||||
},
|
||||
}
|
||||
|
||||
funcDecl := &ast.FuncDecl{
|
||||
Recv: dbRecv,
|
||||
Name: ast.NewIdent("Delete" + tbl.GoTypeName),
|
||||
Type: &ast.FuncType{Params: arg, Results: nil},
|
||||
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{{
|
||||
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
}}}, Results: nil},
|
||||
Body: funcBody,
|
||||
}
|
||||
return funcDecl
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/jinzhu/inflection"
|
||||
|
||||
pkgschema "git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||
)
|
||||
|
||||
// GenerateModelTestAST produces an AST for a starter test file for a given model.
|
||||
@@ -15,9 +16,11 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
packageName := "db"
|
||||
testpackageName := packageName + "_test"
|
||||
|
||||
makeHelperName := ast.NewIdent("Make" + tbl.GoTypeName)
|
||||
|
||||
// func MakeItem() Item { return Item{} }
|
||||
makeItemFunc := &ast.FuncDecl{
|
||||
Name: ast.NewIdent("Make" + tbl.GoTypeName),
|
||||
Name: makeHelperName,
|
||||
Type: &ast.FuncType{
|
||||
Params: &ast.FieldList{},
|
||||
Results: &ast.FieldList{
|
||||
@@ -32,6 +35,24 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
Results: []ast.Expr{
|
||||
&ast.CompositeLit{
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
Elts: []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: ast.NewIdent("Data"),
|
||||
Value: &ast.CompositeLit{
|
||||
Type: &ast.ArrayType{
|
||||
Elt: ast.NewIdent("byte"),
|
||||
},
|
||||
Elts: []ast.Expr{},
|
||||
},
|
||||
},
|
||||
&ast.KeyValueExpr{
|
||||
Key: ast.NewIdent("Description"),
|
||||
Value: &ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: `""`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -39,9 +60,9 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
}
|
||||
|
||||
testObj := ast.NewIdent("item")
|
||||
testObj2 := ast.NewIdent("item2")
|
||||
fieldName := ast.NewIdent("Description")
|
||||
testObj := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName))
|
||||
testObj2 := ast.NewIdent(textutils.CamelToPascal(tbl.GoTypeName) + "2")
|
||||
fieldName := ast.NewIdent("Description") // TODO
|
||||
description1 := `"an item"`
|
||||
description2 := `"a big item"`
|
||||
testDB := ast.NewIdent("TestDB")
|
||||
@@ -72,19 +93,27 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
stmts := []ast.Stmt{
|
||||
Comment("Create"),
|
||||
|
||||
// item := Item{Description: "an item"}
|
||||
// item := MakeItem()
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{testObj},
|
||||
Tok: token.DEFINE,
|
||||
Rhs: []ast.Expr{&ast.CompositeLit{
|
||||
Type: ast.NewIdent(tbl.GoTypeName),
|
||||
Elts: []ast.Expr{
|
||||
&ast.KeyValueExpr{
|
||||
Key: fieldName,
|
||||
Value: &ast.BasicLit{Kind: token.STRING, Value: description1},
|
||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: makeHelperName, Args: nil}},
|
||||
},
|
||||
// item.Description = "an item"
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
&ast.SelectorExpr{
|
||||
X: testObj,
|
||||
Sel: ast.NewIdent("Description"),
|
||||
},
|
||||
},
|
||||
Tok: token.ASSIGN,
|
||||
Rhs: []ast.Expr{
|
||||
&ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: fmt.Sprintf("%q", description1),
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
|
||||
// TestDB.SaveItem(&item)
|
||||
@@ -122,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{
|
||||
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: testObj, Sel: fieldName},
|
||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||
},
|
||||
}},
|
||||
@@ -281,11 +310,11 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
},
|
||||
}
|
||||
shouldDefineErr := true
|
||||
for _, col := range tbl.Columns {
|
||||
if col.IsForeignKey {
|
||||
shouldIncludeTestFkCheck = true
|
||||
stmts = append(stmts, []ast.Stmt{
|
||||
|
||||
// post.QuotedPostID = 94354538969386985
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{
|
||||
@@ -306,7 +335,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
// err := db.SavePost(&post)
|
||||
&ast.AssignStmt{
|
||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||
Tok: token.DEFINE,
|
||||
Tok: map[bool]token.Token{true: token.DEFINE, false: token.ASSIGN}[shouldDefineErr],
|
||||
Rhs: []ast.Expr{
|
||||
&ast.CallExpr{
|
||||
Fun: &ast.SelectorExpr{
|
||||
@@ -342,6 +371,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
|
||||
},
|
||||
},
|
||||
}...)
|
||||
shouldDefineErr = false
|
||||
}
|
||||
}
|
||||
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,85 +1,14 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"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:]
|
||||
}
|
||||
|
||||
func (c Column) GoTypeName() string {
|
||||
if c.IsNonCodeTableForeignKey() {
|
||||
return TypenameFromTablename(c.ForeignKeyTargetTable) + "ID"
|
||||
}
|
||||
switch c.Type {
|
||||
case "integer", "int":
|
||||
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
|
||||
return "bool"
|
||||
} else if strings.HasSuffix(c.Name, "_at") {
|
||||
return "Timestamp"
|
||||
}
|
||||
return "int"
|
||||
case "text":
|
||||
return "string"
|
||||
case "real":
|
||||
return "float32"
|
||||
case "blob":
|
||||
return "[]byte"
|
||||
default:
|
||||
panic("Unrecognized sqlite column type: " + c.Type)
|
||||
}
|
||||
}
|
||||
|
||||
// Table is a single SQLite table.
|
||||
type Table struct {
|
||||
TableName string `db:"name"`
|
||||
@@ -138,16 +67,10 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
func (t Table) GetCodeTableValues(db *sqlx.DB) (ret []string) {
|
||||
if !slices.ContainsFunc(t.Columns, func(c Column) bool { return c.Name == "name" }) {
|
||||
panic("not a code table")
|
||||
}
|
||||
|
||||
type Schema struct {
|
||||
Tables map[string]Table
|
||||
Indexes map[string]Index
|
||||
flowutils.PanicIf(db.Select(&ret, fmt.Sprintf("select name from %s", t.TableName)))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,3 +9,17 @@ func SnakeToCamel(s string) string {
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func KebabToPascal(s string) string {
|
||||
parts := strings.Split(s, "-")
|
||||
for i, part := range parts {
|
||||
if len(part) > 0 {
|
||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func CamelToPascal(s string) string {
|
||||
return strings.ToLower(s)[0:1] + s[1:]
|
||||
}
|
||||
|
||||
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