Compare commits
24 Commits
v0.0.1
...
d6426bba14
| Author | SHA1 | Date | |
|---|---|---|---|
| d6426bba14 | |||
| 8ab21edae9 | |||
| ee1d0a5ed7 | |||
| 93b589d1b2 | |||
| b0d95c5948 | |||
| 85d544152f | |||
| 5cbb657666 | |||
| b8a024a4b9 | |||
| 4cba2af670 | |||
| 04676461ff | |||
| a36058fdbe | |||
| f0c152cfe4 | |||
| 5973a2a4b7 | |||
| 75b7662c34 | |||
| 229f41e478 | |||
| 2e50736e67 | |||
| 1c56661560 | |||
| 3d357abb93 | |||
|
|
a3da7573c1 | ||
|
|
b96ab19bc2 | ||
|
|
cb8edd74c0 | ||
|
|
a0d0461f06 | ||
|
|
e85a68e69d | ||
|
|
eee6714918 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1 +1,6 @@
|
|||||||
sample_data/data
|
sample_data/data
|
||||||
|
|
||||||
|
# Legacy versions
|
||||||
|
.cmd-old/
|
||||||
|
pkg/.testapp
|
||||||
|
.claude
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go/ast"
|
"go/ast"
|
||||||
"go/printer"
|
|
||||||
"go/token"
|
"go/token"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@@ -37,46 +36,49 @@ var generate_model = &cobra.Command{
|
|||||||
return ErrNoSuchTable
|
return ErrNoSuchTable
|
||||||
}
|
}
|
||||||
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
|
|
||||||
if Must(cmd.Flags().GetBool("test")) {
|
if Must(cmd.Flags().GetBool("test")) {
|
||||||
file2 := modelgenerate.GenerateModelTestAST(table, modname)
|
file2 := modelgenerate.GenerateModelTestAST(table, modname)
|
||||||
PanicIf(printer.Fprint(os.Stdout, fset, file2))
|
PanicIf(modelgenerate.FprintWithComments(os.Stdout, file2))
|
||||||
} else {
|
} else {
|
||||||
file := &ast.File{
|
decls := []ast.Decl{
|
||||||
Name: ast.NewIdent("db"), // TODO: parameterize
|
&ast.GenDecl{
|
||||||
|
Tok: token.IMPORT,
|
||||||
Decls: []ast.Decl{
|
Specs: []ast.Spec{
|
||||||
&ast.GenDecl{
|
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"database/sql"`}},
|
||||||
Tok: token.IMPORT,
|
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"errors"`}},
|
||||||
Specs: []ast.Spec{
|
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"database/sql"`}},
|
&ast.ImportSpec{
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"errors"`}},
|
Name: ast.NewIdent("."),
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"`},
|
||||||
&ast.ImportSpec{
|
},
|
||||||
Name: ast.NewIdent("."),
|
&ast.ImportSpec{
|
||||||
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"`},
|
Name: ast.NewIdent("."),
|
||||||
},
|
Path: &ast.BasicLit{
|
||||||
&ast.ImportSpec{
|
Kind: token.STRING,
|
||||||
Name: ast.NewIdent("."),
|
Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"`,
|
||||||
Path: &ast.BasicLit{
|
|
||||||
Kind: token.STRING,
|
|
||||||
Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"`,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modelgenerate.GenerateIDType(table),
|
|
||||||
modelgenerate.GenerateModelAST(table),
|
|
||||||
modelgenerate.GenerateSQLFieldsConst(table),
|
|
||||||
modelgenerate.GenerateSaveItemFunc(table),
|
|
||||||
modelgenerate.GenerateDeleteItemFunc(table),
|
|
||||||
modelgenerate.GenerateGetItemByIDFunc(table),
|
|
||||||
modelgenerate.GenerateGetAllItemsFunc(table),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
if !table.IsWithoutRowid {
|
||||||
|
decls = append(decls, modelgenerate.GenerateIDType(table))
|
||||||
|
}
|
||||||
|
decls = append(decls,
|
||||||
|
modelgenerate.GenerateModelAST(table),
|
||||||
|
modelgenerate.GenerateSQLFieldsConst(table),
|
||||||
|
modelgenerate.GenerateSaveItemFunc(table),
|
||||||
|
modelgenerate.GenerateDeleteItemFunc(table),
|
||||||
|
modelgenerate.GenerateGetItemByIDFunc(table),
|
||||||
|
modelgenerate.GenerateGetAllItemsFunc(table),
|
||||||
|
)
|
||||||
|
|
||||||
PanicIf(printer.Fprint(os.Stdout, fset, file))
|
file := &ast.File{
|
||||||
|
Name: ast.NewIdent("db"), // TODO: parameterize
|
||||||
|
Decls: decls,
|
||||||
|
}
|
||||||
|
|
||||||
|
PanicIf(modelgenerate.FprintWithComments(os.Stdout, file))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
34
doc/TODO.txt
34
doc/TODO.txt
@@ -1,17 +1,31 @@
|
|||||||
TODO: auto-timestamps
|
TODO: soft-deletion
|
||||||
- SaveXyz should set created_at and updated_at; shouldn't touch is_deleted or deleted_at
|
- enable soft-deletion if table has `is_deleted` and `deleted_at` fields
|
||||||
- if soft delete is enabled, DeleteXyz should do update (not delete) and set is_deleted and deleted_at
|
- if soft delete is enabled, DeleteXyz should do update (not delete) and set is_deleted and deleted_at
|
||||||
- ...and DeleteXyz should have pointer receiver for soft-delete
|
- ...and DeleteXyz should have pointer receiver for soft-delete
|
||||||
- SaveXyz shouldn't set created_at in the do-update branch
|
|
||||||
- GetXyzByID should include `ErrItemIsDeleted` if item is soft-deleted
|
- GetXyzByID should include `ErrItemIsDeleted` if item is soft-deleted
|
||||||
|
|
||||||
TODO: modified-timestamps
|
|
||||||
- set updated_at and created_at in SaveXYZ
|
|
||||||
- soft delete option
|
|
||||||
|
|
||||||
TODO: generator-foreign-keys
|
|
||||||
- add auto-foreign-key checking blocks to SaveXyz
|
|
||||||
|
|
||||||
TODO: migration-structs
|
TODO: migration-structs
|
||||||
- Right now, migrations are strings. Could be a struct with "name", "up" and "down" fields
|
- Right now, migrations are strings. Could be a struct with "name", "up" and "down" fields
|
||||||
- Adding a "down" operation enables handling newer DB versions with "down instead of error-out" for development (perhaps a flag)
|
- Adding a "down" operation enables handling newer DB versions with "down instead of error-out" for development (perhaps a flag)
|
||||||
|
|
||||||
|
IDEA: migrations-table
|
||||||
|
- Store migrations in a table. This makes the schema more self-documenting.
|
||||||
|
- Possible schema: name, sql_up, sql_down, hash (computed from those fields plus previous migration hash)
|
||||||
|
- or just rowid instead of hash? Migration sequence should be immutable after publishing, so there should never be "conflicts"
|
||||||
|
|
||||||
|
TODO: language-server-for-TODO.txt
|
||||||
|
|
||||||
|
TODO: auto-migration-checker
|
||||||
|
- Use `pkg/schema` to test whether a base schema plus a migration equals a new schema
|
||||||
|
|
||||||
|
TODO: codegen SaveXyz update path doesn't check foreign keys
|
||||||
|
- Insert path has FK error handling, but the update path wraps everything in Must
|
||||||
|
- An update that violates an FK constraint will panic instead of returning an error
|
||||||
|
|
||||||
|
TODO: codegen `without rowid` tables properly
|
||||||
|
|
||||||
|
TODO: generated test file inclues global test DB setup, which is wrong
|
||||||
|
|
||||||
|
TODO: join-tables
|
||||||
|
- handle codegen for without rowid tables properly
|
||||||
|
- add "get all "
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -3,6 +3,7 @@ module git.offline-twitter.com/offline-labs/gas-stack
|
|||||||
go 1.22.5
|
go 1.22.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/go-test/deep v1.1.1
|
||||||
github.com/jinzhu/inflection v1.0.0
|
github.com/jinzhu/inflection v1.0.0
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
|
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
|
||||||
|
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ RUN apk add less
|
|||||||
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v2.0.2
|
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v2.0.2
|
||||||
RUN GOBIN=/usr/local/bin go install git.offline-twitter.com/offline-labs/gocheckout@v0.0.2
|
RUN GOBIN=/usr/local/bin go install git.offline-twitter.com/offline-labs/gocheckout@v0.0.2
|
||||||
|
|
||||||
|
COPY etc/group /etc/group
|
||||||
|
|
||||||
# Create a user in the container with the same UID as on the host machine, to avoid ownership conflicts.
|
# Create a user in the container with the same UID as on the host machine, to avoid ownership conflicts.
|
||||||
# The user gets sudo of course.
|
# The user gets sudo of course.
|
||||||
#
|
#
|
||||||
|
|||||||
3
ops/devcontainer/etc/group
Normal file
3
ops/devcontainer/etc/group
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
root:x:0:
|
||||||
|
nogroup:x:65533:
|
||||||
|
nobody:x:65534:
|
||||||
@@ -7,6 +7,7 @@ sudo docker run --rm -it \
|
|||||||
-v "$(go env GOMODCACHE):/gocache-vol/mod-cache" \
|
-v "$(go env GOMODCACHE):/gocache-vol/mod-cache" \
|
||||||
-e GOMODCACHE=/gocache-vol/mod-cache \
|
-e GOMODCACHE=/gocache-vol/mod-cache \
|
||||||
-e GOLANGCI_LINT_CACHE=/gocache-vol/lint-cache \
|
-e GOLANGCI_LINT_CACHE=/gocache-vol/lint-cache \
|
||||||
|
-v /memory:/memory \
|
||||||
--workdir /code \
|
--workdir /code \
|
||||||
--net host \
|
--net host \
|
||||||
gas
|
gas
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ set -e
|
|||||||
set -x
|
set -x
|
||||||
|
|
||||||
PS4='+(${BASH_SOURCE}:${LINENO}): '
|
PS4='+(${BASH_SOURCE}:${LINENO}): '
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
proj_root=$(readlink -f "$(dirname "${BASH_SOURCE[0]}")/..")
|
||||||
|
cd "$proj_root"
|
||||||
|
|
||||||
# Compile `gas`
|
# Compile `gas`
|
||||||
gas="/tmp/gas"
|
gas="/tmp/gas"
|
||||||
@@ -29,11 +30,19 @@ EOF
|
|||||||
|
|
||||||
cd $test_project
|
cd $test_project
|
||||||
|
|
||||||
|
# Add "replace" directive"
|
||||||
|
echo "replace git.offline-twitter.com/offline-labs/gas-stack => $proj_root" >> go.mod
|
||||||
|
|
||||||
# Create a new table in the schema
|
# Create a new table in the schema
|
||||||
cat >> pkg/db/schema.sql <<EOF
|
cat >> pkg/db/schema.sql <<EOF
|
||||||
|
create table item_flavor (rowid integer primary key, name text not null) strict;
|
||||||
|
|
||||||
create table items (
|
create table items (
|
||||||
rowid integer primary key,
|
rowid integer primary key,
|
||||||
description text not null default ''
|
description text not null default '',
|
||||||
|
flavor integer references item_flavor(rowid),
|
||||||
|
created_at integer not null,
|
||||||
|
updated_at integer not null
|
||||||
) strict;
|
) strict;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
180
pkg/codegen/modelgenerate/ast_helpers.go
Normal file
180
pkg/codegen/modelgenerate/ast_helpers.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package modelgenerate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"go/ast"
|
||||||
|
"go/parser"
|
||||||
|
"go/printer"
|
||||||
|
"go/token"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These just need to be unique (i.e., something that will never be a real function in actual code).
|
||||||
|
const (
|
||||||
|
commentMarker = "__comment__jafkjewfkajwlekfjawlejf"
|
||||||
|
blankLineMarker = "__blank_line__awefjakwlefjkwalkefj"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrailingComments is a side map for attaching end-of-line comments to AST nodes.
|
||||||
|
// Generators populate this map; FprintWithComments consumes and clears it.
|
||||||
|
// For a regular statement, the comment appears at the end of the statement's line.
|
||||||
|
// For a BlockStmt, the comment appears after the closing brace.
|
||||||
|
//
|
||||||
|
// This is a terrible implementation (global variable), but Node is an interface (pointer type),
|
||||||
|
// so there's no risk of cross-contamination really. Still bad for concurrent use, but hopefully
|
||||||
|
// we don't have to do that.
|
||||||
|
var TrailingComments = map[ast.Node]string{}
|
||||||
|
|
||||||
|
// mustCall wraps a call expression in Must(...), producing AST for Must(inner).
|
||||||
|
func mustCall(inner ast.Expr) *ast.CallExpr {
|
||||||
|
return &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("Must"),
|
||||||
|
Args: []ast.Expr{inner},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment creates a marker statement that will be converted to a real Go comment
|
||||||
|
// in the generated output by FprintWithComments.
|
||||||
|
func Comment(text string) ast.Stmt {
|
||||||
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent(commentMarker),
|
||||||
|
Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: `"` + text + `"`}},
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BlankLine creates a marker statement that will be converted to a blank line
|
||||||
|
// in the generated output. The marker occupies a line in the first-pass output;
|
||||||
|
// when removed, the position gap causes the printer to insert a blank line.
|
||||||
|
func BlankLine() *ast.ExprStmt {
|
||||||
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent(blankLineMarker),
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FprintWithComments prints an ast.File, converting Comment/BlankLine markers
|
||||||
|
// into real Go comments and blank lines, preserving Doc comments, and applying
|
||||||
|
// trailing comments from the TrailingComments side map.
|
||||||
|
//
|
||||||
|
// It does a round-trip (print -> parse -> modify -> print) to obtain real token
|
||||||
|
// positions, which Go's comment system requires.
|
||||||
|
func FprintWithComments(w io.Writer, file *ast.File) error {
|
||||||
|
// First pass: print to buffer
|
||||||
|
// Doc comments on FuncDecl/GenDecl are NOT added to file.Comments here;
|
||||||
|
// the printer handles them via setComment(d.Doc) when visiting each decl.
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fset := token.NewFileSet()
|
||||||
|
if err := printer.Fprint(&buf, fset, file); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-parse to get real positions (ParseComments preserves doc comments)
|
||||||
|
fset = token.NewFileSet()
|
||||||
|
parsed, err := parser.ParseFile(fset, "", buf.Bytes(), parser.ParseComments)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the tree-of-nodes into a slice-of-nodes
|
||||||
|
collectNodes := func(node ast.Node) []ast.Node {
|
||||||
|
var nodes []ast.Node
|
||||||
|
ast.Inspect(node, func(n ast.Node) bool {
|
||||||
|
// Filter out comments, as they only appear in the
|
||||||
|
switch n.(type) {
|
||||||
|
case *ast.CommentGroup, *ast.Comment:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
nodes = append(nodes, n)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parallel walk: apply TrailingComments from the side map.
|
||||||
|
// Both trees have identical structure (the reparse is just a positioned copy),
|
||||||
|
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
|
||||||
|
// avoid mismatches from Doc fields.
|
||||||
|
if len(TrailingComments) > 0 {
|
||||||
|
origNodes := collectNodes(file)
|
||||||
|
reparsedNodes := collectNodes(parsed)
|
||||||
|
for i, orig := range origNodes {
|
||||||
|
text, ok := TrailingComments[orig]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
reparsed := reparsedNodes[i]
|
||||||
|
parsed.Comments = append(parsed.Comments, &ast.CommentGroup{
|
||||||
|
List: []*ast.Comment{{Slash: reparsed.End(), Text: "// " + text}},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extractCommentMarker := func(stmt ast.Stmt) (string, bool) {
|
||||||
|
expr, ok := stmt.(*ast.ExprStmt)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
call, ok := expr.X.(*ast.CallExpr)
|
||||||
|
if !ok {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
ident, ok := call.Fun.(*ast.Ident)
|
||||||
|
if !ok || ident.Name != commentMarker {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
lit := call.Args[0].(*ast.BasicLit)
|
||||||
|
return lit.Value[1 : len(lit.Value)-1], true
|
||||||
|
}
|
||||||
|
|
||||||
|
isBlankLineMarker := func(stmt ast.Stmt) bool {
|
||||||
|
expr, ok := stmt.(*ast.ExprStmt)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
call, ok := expr.X.(*ast.CallExpr)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
ident, ok := call.Fun.(*ast.Ident)
|
||||||
|
return ok && ident.Name == blankLineMarker
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert comment and blank-line markers
|
||||||
|
ast.Inspect(parsed, func(n ast.Node) bool {
|
||||||
|
// We only care about Block nodes
|
||||||
|
block, ok := n.(*ast.BlockStmt)
|
||||||
|
if !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the statements in this block , replacing our artificial Comment(...) and BlankLine(...) nodes
|
||||||
|
// in the block statement's body with actual ones
|
||||||
|
filtered := block.List[:0]
|
||||||
|
for _, stmt := range block.List {
|
||||||
|
if text, ok := extractCommentMarker(stmt); ok {
|
||||||
|
// If it's a comment, add it to the fileset's list of Comments
|
||||||
|
parsed.Comments = append(parsed.Comments, &ast.CommentGroup{
|
||||||
|
List: []*ast.Comment{{Slash: stmt.Pos(), Text: "// " + text}},
|
||||||
|
})
|
||||||
|
} else if isBlankLineMarker(stmt) {
|
||||||
|
// If it's a blank line, just remove it; the position gap creates a blank line
|
||||||
|
} else {
|
||||||
|
// Otherwise: it's a normal statement, so keep it.
|
||||||
|
filtered = append(filtered, stmt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
block.List = filtered
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
sort.Slice(parsed.Comments, func(i, j int) bool {
|
||||||
|
return parsed.Comments[i].Pos() < parsed.Comments[j].Pos()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Clear side map for next invocation
|
||||||
|
for k := range TrailingComments {
|
||||||
|
delete(TrailingComments, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
return printer.Fprint(w, fset, parsed)
|
||||||
|
}
|
||||||
@@ -13,6 +13,25 @@ import (
|
|||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------
|
||||||
|
// Generators
|
||||||
|
// ---------------
|
||||||
|
|
||||||
|
// GenerateIDType produces an AST for the model's ID field.
|
||||||
func GenerateIDType(table schema.Table) *ast.GenDecl {
|
func GenerateIDType(table schema.Table) *ast.GenDecl {
|
||||||
// e.g., `type FoodID int`
|
// e.g., `type FoodID int`
|
||||||
return &ast.GenDecl{
|
return &ast.GenDecl{
|
||||||
@@ -37,33 +56,14 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
|||||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: "`db:\"rowid\" json:\"id\"`"},
|
Tag: &ast.BasicLit{Kind: token.STRING, Value: "`db:\"rowid\" json:\"id\"`"},
|
||||||
})
|
})
|
||||||
default:
|
default:
|
||||||
if col.IsForeignKey && strings.HasSuffix(col.Name, "_id") {
|
if col.IsNonCodeTableForeignKey() {
|
||||||
fields = append(fields, &ast.Field{
|
fields = append(fields, &ast.Field{
|
||||||
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(strings.TrimSuffix(col.Name, "_id")) + "ID")},
|
Names: []*ast.Ident{ast.NewIdent(col.GoFieldName())},
|
||||||
Type: ast.NewIdent(schema.TypenameFromTablename(col.ForeignKeyTargetTable) + "ID"),
|
Type: ast.NewIdent(schema.TypenameFromTablename(col.ForeignKeyTargetTable) + "ID"),
|
||||||
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
typeName := "string"
|
typeName := col.GoType()
|
||||||
switch col.Type {
|
|
||||||
case "integer", "int":
|
|
||||||
if strings.HasPrefix(col.Name, "is_") || strings.HasPrefix(col.Name, "has_") {
|
|
||||||
typeName = "bool"
|
|
||||||
} else if strings.HasSuffix(col.Name, "_at") {
|
|
||||||
typeName = "Timestamp"
|
|
||||||
} else {
|
|
||||||
typeName = "int64"
|
|
||||||
}
|
|
||||||
case "text":
|
|
||||||
typeName = "string"
|
|
||||||
case "real":
|
|
||||||
typeName = "float32"
|
|
||||||
case "blob":
|
|
||||||
typeName = "[]byte"
|
|
||||||
default:
|
|
||||||
panic("Unrecognized sqlite column type: " + col.Type)
|
|
||||||
}
|
|
||||||
|
|
||||||
fields = append(fields, &ast.Field{
|
fields = append(fields, &ast.Field{
|
||||||
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))},
|
Names: []*ast.Ident{ast.NewIdent(textutils.SnakeToCamel(col.Name))},
|
||||||
Type: ast.NewIdent(typeName),
|
Type: ast.NewIdent(typeName),
|
||||||
@@ -82,6 +82,129 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildFKCheckLambda builds the `checkForeignKeyFailures := func(err error) error { ... }` AST.
|
||||||
|
// Returns the assignment statement and whether any FK columns were found.
|
||||||
|
func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
||||||
|
hasFks := false
|
||||||
|
stmt := &ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("checkForeignKeyFailures")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{
|
||||||
|
&ast.FuncLit{
|
||||||
|
Type: &ast.FuncType{
|
||||||
|
Params: &ast.FieldList{
|
||||||
|
List: []*ast.Field{{
|
||||||
|
Names: []*ast.Ident{ast.NewIdent("err")},
|
||||||
|
Type: ast.NewIdent("error"),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Results: &ast.FieldList{
|
||||||
|
List: []*ast.Field{{Type: ast.NewIdent("error")}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: func() []ast.Stmt {
|
||||||
|
ret := []ast.Stmt{}
|
||||||
|
// if !isSqliteFkError(err) { return nil }
|
||||||
|
ret = append(ret, &ast.IfStmt{
|
||||||
|
Cond: &ast.UnaryExpr{Op: token.NOT, X: &ast.CallExpr{Fun: ast.NewIdent("IsSqliteFkError"), Args: []ast.Expr{ast.NewIdent("err")}}},
|
||||||
|
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}}}},
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, col := range tbl.Columns {
|
||||||
|
if !col.IsForeignKey { // Check both "real" foreign keys and code table values
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hasFks = true
|
||||||
|
|
||||||
|
structFieldName := col.GoFieldName()
|
||||||
|
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
||||||
|
|
||||||
|
if col.IsNonCodeTableForeignKey() {
|
||||||
|
// Real foreign key; look up referent by ID to see if it exists
|
||||||
|
ret = append(ret, &ast.IfStmt{
|
||||||
|
Init: &ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{
|
||||||
|
&ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent(getByIDFuncName(col.ForeignKeyTargetTable))},
|
||||||
|
Args: []ast.Expr{structField},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cond: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||||
|
Args: []ast.Expr{ast.NewIdent("err"), ast.NewIdent("ErrNotInDB")},
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
&ast.ReturnStmt{
|
||||||
|
Results: []ast.Expr{
|
||||||
|
&ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||||
|
Args: []ast.Expr{
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
|
||||||
|
structField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Code table value. Query the table to see if it exists
|
||||||
|
ret = append(ret, &ast.IfStmt{
|
||||||
|
Init: &ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||||
|
Tok: token.ASSIGN,
|
||||||
|
Rhs: []ast.Expr{
|
||||||
|
&ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")},
|
||||||
|
Args: []ast.Expr{
|
||||||
|
&ast.CallExpr{Fun: ast.NewIdent("new"), Args: []ast.Expr{ast.NewIdent("int")}},
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`select 1 from %s where rowid = ?`", col.ForeignKeyTargetTable)},
|
||||||
|
structField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cond: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
||||||
|
Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}},
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
&ast.ReturnStmt{
|
||||||
|
Results: []ast.Expr{
|
||||||
|
&ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("NewForeignKeyError"),
|
||||||
|
Args: []ast.Expr{
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
|
||||||
|
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)),
|
||||||
|
structField,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// final return nil
|
||||||
|
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
|
||||||
|
return ret
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return stmt, hasFks
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateSaveItemFunc produces an AST for the SaveXyz() function of the model.
|
// GenerateSaveItemFunc produces an AST for the SaveXyz() function of the model.
|
||||||
// E.g., a table with `table.TypeName = "foods"` will produce a "SaveFood()" function.
|
// E.g., a table with `table.TypeName = "foods"` will produce a "SaveFood()" function.
|
||||||
func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
||||||
@@ -89,6 +212,9 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
insertVals := make([]string, 0, len(tbl.Columns))
|
insertVals := make([]string, 0, len(tbl.Columns))
|
||||||
updatePairs := make([]string, 0, len(tbl.Columns))
|
updatePairs := make([]string, 0, len(tbl.Columns))
|
||||||
|
|
||||||
|
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
|
||||||
|
|
||||||
|
// Assemble data for building SQL "insert" and "update" strings
|
||||||
for _, col := range tbl.Columns {
|
for _, col := range tbl.Columns {
|
||||||
if col.Name == "rowid" {
|
if col.Name == "rowid" {
|
||||||
continue
|
continue
|
||||||
@@ -99,104 +225,200 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
val = fmt.Sprintf("nullif(%s, 0)", val)
|
val = fmt.Sprintf("nullif(%s, 0)", val)
|
||||||
}
|
}
|
||||||
insertVals = append(insertVals, val)
|
insertVals = append(insertVals, val)
|
||||||
|
// created_at should not be updated after creation
|
||||||
|
if col.Name == "created_at" && hasCreatedAt {
|
||||||
|
continue
|
||||||
|
}
|
||||||
updatePairs = append(updatePairs, col.Name+"="+val)
|
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, ", "))
|
||||||
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 "))
|
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)
|
||||||
|
|
||||||
funcBody := &ast.BlockStmt{
|
funcBody := &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: func() []ast.Stmt {
|
||||||
&ast.IfStmt{
|
ret := []ast.Stmt{}
|
||||||
|
if hasFks {
|
||||||
|
ret = append(ret, checkForeignKeyFailuresAssignment, BlankLine())
|
||||||
|
}
|
||||||
|
if hasUpdatedAt {
|
||||||
|
// Auto-timestamps: updated_at
|
||||||
|
ret = append(ret, &ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("UpdatedAt")}},
|
||||||
|
Tok: token.ASSIGN,
|
||||||
|
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{
|
Cond: &ast.BinaryExpr{
|
||||||
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||||
Op: token.EQL,
|
Op: token.EQL,
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
Y: &ast.BasicLit{Kind: token.INT, Value: "0"},
|
||||||
},
|
},
|
||||||
Body: &ast.BlockStmt{
|
Body: &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
// Do create
|
||||||
&ast.AssignStmt{
|
List: append(
|
||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
func() []ast.Stmt {
|
||||||
Tok: token.DEFINE,
|
ret1 := []ast.Stmt{Comment("Do create")}
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
if hasCreatedAt {
|
||||||
Fun: ast.NewIdent("Must"),
|
// Auto-timestamps: created_at
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
ret1 = append(ret1, &ast.AssignStmt{
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")},
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
|
||||||
Args: []ast.Expr{
|
Tok: token.ASSIGN,
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||||
ast.NewIdent(tbl.VarName),
|
})
|
||||||
|
}
|
||||||
|
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{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}},
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")}},
|
||||||
Tok: token.ASSIGN,
|
Tok: token.ASSIGN,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
Rhs: []ast.Expr{&ast.CallExpr{
|
||||||
Fun: ast.NewIdent(tbl.TypeIDName),
|
Fun: ast.NewIdent(tbl.TypeIDName),
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{mustCall(&ast.CallExpr{
|
||||||
Fun: ast.NewIdent("Must"),
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")},
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{},
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("LastInsertId")},
|
})},
|
||||||
Args: []ast.Expr{},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
Else: &ast.BlockStmt{
|
Else: &ast.BlockStmt{
|
||||||
|
// Do update
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
|
Comment("Do update"),
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||||
Fun: ast.NewIdent("Must"),
|
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("NamedExec")},
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"}, ast.NewIdent(tbl.VarName)},
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")},
|
})},
|
||||||
Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: "`" + updateStmt + "`"}, ast.NewIdent(tbl.VarName)},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
|
|
||||||
&ast.IfStmt{
|
&ast.IfStmt{
|
||||||
Cond: &ast.BinaryExpr{
|
Cond: &ast.BinaryExpr{
|
||||||
X: &ast.CallExpr{
|
X: mustCall(&ast.CallExpr{
|
||||||
Fun: ast.NewIdent("Must"),
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{},
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")},
|
}),
|
||||||
Args: []ast.Expr{},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
Op: token.NEQ,
|
Op: token.NEQ,
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
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: ast.NewIdent("fmt.Errorf"), 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")}}}}}}}},
|
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 {
|
||||||
|
// 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")}})
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}(),
|
||||||
}
|
}
|
||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
funcDecl := &ast.FuncDecl{
|
||||||
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}},
|
Doc: &ast.CommentGroup{List: []*ast.Comment{
|
||||||
|
{Text: fmt.Sprintf("// Save%s creates or updates a %s in the database.", tbl.GoTypeName, tbl.GoTypeName)},
|
||||||
|
{Text: "// If the item doesn't exist (has no ID set), it will create it; otherwise it will do an update."},
|
||||||
|
}},
|
||||||
|
Recv: dbRecv,
|
||||||
Name: ast.NewIdent("Save" + tbl.GoTypeName),
|
Name: ast.NewIdent("Save" + tbl.GoTypeName),
|
||||||
Type: &ast.FuncType{
|
Type: &ast.FuncType{
|
||||||
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)}}}},
|
Params: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: &ast.StarExpr{X: ast.NewIdent(tbl.GoTypeName)}}}},
|
||||||
Results: nil,
|
Results: func() *ast.FieldList {
|
||||||
|
if hasFks {
|
||||||
|
return &ast.FieldList{List: []*ast.Field{{Type: ast.NewIdent("error")}}}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}(),
|
||||||
},
|
},
|
||||||
Body: funcBody,
|
Body: funcBody,
|
||||||
}
|
}
|
||||||
return funcDecl
|
return funcDecl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getByIDFuncName(tblname string) string {
|
||||||
|
return "Get" + schema.TypenameFromTablename(tblname) + "ByID"
|
||||||
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
funcName := "Get" + tbl.GoTypeName + "ByID"
|
|
||||||
|
|
||||||
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
|
|
||||||
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}}
|
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("id")}, Type: ast.NewIdent(tbl.TypeIDName)}}}
|
||||||
result := &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")}}}
|
result := &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")}}}
|
||||||
|
|
||||||
@@ -216,7 +438,7 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||||
Tok: token.ASSIGN,
|
Tok: token.ASSIGN,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Get")}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, selectExpr, ast.NewIdent("id")}}},
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Get")}, Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")}, selectExpr, ast.NewIdent("id")}}},
|
||||||
},
|
},
|
||||||
&ast.IfStmt{
|
&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")}}},
|
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")}}},
|
||||||
@@ -227,8 +449,8 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
funcDecl := &ast.FuncDecl{
|
||||||
Recv: recv,
|
Recv: dbRecv,
|
||||||
Name: ast.NewIdent(funcName),
|
Name: ast.NewIdent(getByIDFuncName(tbl.TableName)),
|
||||||
Type: &ast.FuncType{Params: arg, Results: result},
|
Type: &ast.FuncType{Params: arg, Results: result},
|
||||||
Body: funcBody,
|
Body: funcBody,
|
||||||
}
|
}
|
||||||
@@ -239,9 +461,6 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
// E.g., a table with `table.TypeName = "foods"` will produce a "GetAllFoods()" function.
|
// E.g., a table with `table.TypeName = "foods"` will produce a "GetAllFoods()" function.
|
||||||
func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
||||||
funcName := "GetAll" + inflection.Plural(tbl.GoTypeName)
|
funcName := "GetAll" + inflection.Plural(tbl.GoTypeName)
|
||||||
recv := &ast.FieldList{List: []*ast.Field{
|
|
||||||
{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")},
|
|
||||||
}}
|
|
||||||
result := &ast.FieldList{List: []*ast.Field{
|
result := &ast.FieldList{List: []*ast.Field{
|
||||||
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.GoTypeName)}},
|
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: &ast.ArrayType{Elt: ast.NewIdent(tbl.GoTypeName)}},
|
||||||
}}
|
}}
|
||||||
@@ -251,19 +470,19 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
Args: []ast.Expr{
|
Args: []ast.Expr{
|
||||||
&ast.CallExpr{
|
&ast.CallExpr{
|
||||||
Fun: &ast.SelectorExpr{
|
Fun: &ast.SelectorExpr{
|
||||||
X: ast.NewIdent("db.DB"),
|
X: dbDB,
|
||||||
Sel: ast.NewIdent("Select"),
|
Sel: ast.NewIdent("Select"),
|
||||||
},
|
},
|
||||||
Args: []ast.Expr{
|
Args: []ast.Expr{
|
||||||
&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")},
|
&ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("ret")},
|
||||||
&ast.BinaryExpr{
|
&ast.BinaryExpr{
|
||||||
X: &ast.BinaryExpr{
|
X: &ast.BinaryExpr{
|
||||||
X: &ast.BasicLit{Kind: token.STRING, Value: "`SELECT `"},
|
X: &ast.BasicLit{Kind: token.STRING, Value: "`select `"},
|
||||||
Op: token.ADD,
|
Op: token.ADD,
|
||||||
Y: SQLFieldsConstIdent(tbl),
|
Y: SQLFieldsConstIdent(tbl),
|
||||||
},
|
},
|
||||||
Op: token.ADD,
|
Op: token.ADD,
|
||||||
Y: &ast.BasicLit{Kind: token.STRING, Value: "` FROM " + tbl.TableName + "`"},
|
Y: &ast.BasicLit{Kind: token.STRING, Value: "` from " + tbl.TableName + "`"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -278,7 +497,7 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &ast.FuncDecl{
|
return &ast.FuncDecl{
|
||||||
Recv: recv,
|
Recv: dbRecv,
|
||||||
Name: ast.NewIdent(funcName),
|
Name: ast.NewIdent(funcName),
|
||||||
Type: &ast.FuncType{
|
Type: &ast.FuncType{
|
||||||
Params: &ast.FieldList{},
|
Params: &ast.FieldList{},
|
||||||
@@ -291,34 +510,29 @@ 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 {
|
||||||
funcName := "Delete" + tbl.GoTypeName
|
arg := &ast.FieldList{List: []*ast.Field{{
|
||||||
recv := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent("db")}, Type: ast.NewIdent("DB")}}}
|
Names: []*ast.Ident{ast.NewIdent(tbl.VarName)},
|
||||||
arg := &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{ast.NewIdent(tbl.VarName)}, Type: ast.NewIdent(tbl.GoTypeName)}}}
|
Type: ast.NewIdent(tbl.GoTypeName),
|
||||||
|
}}}
|
||||||
|
|
||||||
funcBody := &ast.BlockStmt{
|
funcBody := &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||||
Fun: ast.NewIdent("Must"),
|
Fun: &ast.SelectorExpr{X: dbDB, Sel: ast.NewIdent("Exec")},
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("Exec")},
|
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
|
||||||
Args: []ast.Expr{
|
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: "`delete from " + tbl.TableName + " where rowid = ?`"},
|
},
|
||||||
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
})},
|
||||||
},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
},
|
||||||
&ast.IfStmt{
|
&ast.IfStmt{
|
||||||
Cond: &ast.BinaryExpr{
|
Cond: &ast.BinaryExpr{
|
||||||
X: &ast.CallExpr{
|
X: mustCall(
|
||||||
Fun: ast.NewIdent("Must"),
|
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}},
|
||||||
Args: []ast.Expr{
|
),
|
||||||
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("result"), Sel: ast.NewIdent("RowsAffected")}, Args: []ast.Expr{}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Op: token.NEQ,
|
Op: token.NEQ,
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
||||||
},
|
},
|
||||||
@@ -326,7 +540,7 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
Fun: ast.NewIdent("panic"),
|
Fun: ast.NewIdent("panic"),
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Args: []ast.Expr{&ast.CallExpr{
|
||||||
Fun: ast.NewIdent("fmt.Errorf"),
|
Fun: fmtErrorf,
|
||||||
Args: []ast.Expr{
|
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.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")},
|
&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("ID")},
|
||||||
@@ -339,8 +553,8 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
funcDecl := &ast.FuncDecl{
|
||||||
Recv: recv,
|
Recv: dbRecv,
|
||||||
Name: ast.NewIdent(funcName),
|
Name: ast.NewIdent("Delete" + tbl.GoTypeName),
|
||||||
Type: &ast.FuncType{Params: arg, Results: nil},
|
Type: &ast.FuncType{Params: arg, Results: nil},
|
||||||
Body: funcBody,
|
Body: funcBody,
|
||||||
}
|
}
|
||||||
@@ -351,7 +565,11 @@ func GenerateDeleteItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
|
func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
|
||||||
columns := make([]string, 0, len(tbl.Columns))
|
columns := make([]string, 0, len(tbl.Columns))
|
||||||
for _, col := range tbl.Columns {
|
for _, col := range tbl.Columns {
|
||||||
columns = append(columns, col.Name)
|
if col.IsNullableForeignKey() {
|
||||||
|
columns = append(columns, fmt.Sprintf("ifnull(%s, 0) %s", col.Name, col.Name))
|
||||||
|
} else {
|
||||||
|
columns = append(columns, col.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Join with comma and space
|
// Join with comma and space
|
||||||
value := "`" + strings.Join(columns, ", ") + "`"
|
value := "`" + strings.Join(columns, ", ") + "`"
|
||||||
@@ -366,11 +584,3 @@ func GenerateSQLFieldsConst(tbl schema.Table) *ast.GenDecl {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------
|
|
||||||
// Helpers
|
|
||||||
// ---------------
|
|
||||||
|
|
||||||
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
|
||||||
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,69 +15,25 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
|
|||||||
packageName := "db"
|
packageName := "db"
|
||||||
testpackageName := packageName + "_test"
|
testpackageName := packageName + "_test"
|
||||||
|
|
||||||
testDBDecl := &ast.GenDecl{
|
// func MakeItem() Item { return Item{} }
|
||||||
Tok: token.VAR,
|
makeItemFunc := &ast.FuncDecl{
|
||||||
Specs: []ast.Spec{
|
Name: ast.NewIdent("Make" + tbl.GoTypeName),
|
||||||
&ast.ValueSpec{
|
|
||||||
Names: []*ast.Ident{ast.NewIdent("TestDB")},
|
|
||||||
Type: &ast.StarExpr{X: ast.NewIdent("DB")},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
initFuncDecl := &ast.FuncDecl{
|
|
||||||
Name: ast.NewIdent("init"),
|
|
||||||
Type: &ast.FuncType{Params: &ast.FieldList{}},
|
|
||||||
Body: &ast.BlockStmt{
|
|
||||||
List: []ast.Stmt{
|
|
||||||
&ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{ast.NewIdent("TestDB")},
|
|
||||||
Tok: token.ASSIGN,
|
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("MakeDB"),
|
|
||||||
Args: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: `"tmp"`}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
makeDBHelperDecl := &ast.FuncDecl{
|
|
||||||
Name: ast.NewIdent("MakeDB"),
|
|
||||||
Type: &ast.FuncType{
|
Type: &ast.FuncType{
|
||||||
Params: &ast.FieldList{
|
Params: &ast.FieldList{},
|
||||||
List: []*ast.Field{{
|
|
||||||
Names: []*ast.Ident{ast.NewIdent("dbName")},
|
|
||||||
Type: ast.NewIdent("string"),
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
Results: &ast.FieldList{
|
Results: &ast.FieldList{
|
||||||
List: []*ast.Field{{Type: &ast.StarExpr{X: ast.NewIdent("DB")}}},
|
List: []*ast.Field{
|
||||||
|
{Type: ast.NewIdent(tbl.GoTypeName)},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Body: &ast.BlockStmt{
|
Body: &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
// db := Must(Create(fmt.Sprintf("file:%s?mode=memory&cache=shared", dbName)))
|
|
||||||
&ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{ast.NewIdent("db")},
|
|
||||||
Tok: token.DEFINE,
|
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("Must"),
|
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("Create"),
|
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("fmt.Sprintf"),
|
|
||||||
Args: []ast.Expr{
|
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: `"file:%s?mode=memory&cache=shared"`},
|
|
||||||
ast.NewIdent("dbName"),
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
// return db
|
|
||||||
&ast.ReturnStmt{
|
&ast.ReturnStmt{
|
||||||
Results: []ast.Expr{ast.NewIdent("db")},
|
Results: []ast.Expr{
|
||||||
|
&ast.CompositeLit{
|
||||||
|
Type: ast.NewIdent(tbl.GoTypeName),
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -89,138 +45,157 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
|
|||||||
description1 := `"an item"`
|
description1 := `"an item"`
|
||||||
description2 := `"a big item"`
|
description2 := `"a big item"`
|
||||||
|
|
||||||
|
hasCreatedAt, hasUpdatedAt := tbl.HasAutoTimestamps()
|
||||||
|
|
||||||
|
testFuncType := &ast.FuncType{
|
||||||
|
Params: &ast.FieldList{
|
||||||
|
List: []*ast.Field{{
|
||||||
|
Names: []*ast.Ident{ast.NewIdent("t")},
|
||||||
|
Type: &ast.StarExpr{X: &ast.SelectorExpr{X: ast.NewIdent("testing"), Sel: ast.NewIdent("T")}},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
testCreateUpdateDelete := &ast.FuncDecl{
|
testCreateUpdateDelete := &ast.FuncDecl{
|
||||||
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
|
Name: ast.NewIdent("TestCreateUpdateDelete" + tbl.GoTypeName),
|
||||||
Type: &ast.FuncType{
|
Type: testFuncType,
|
||||||
Params: &ast.FieldList{
|
|
||||||
List: []*ast.Field{{
|
|
||||||
Names: []*ast.Ident{ast.NewIdent("t")},
|
|
||||||
Type: ast.NewIdent("*testing.T"),
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Body: &ast.BlockStmt{
|
Body: &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: func() []ast.Stmt {
|
||||||
// item := Item{Description: "an item"}
|
assertNotZero := func(obj *ast.Ident, field string) *ast.ExprStmt {
|
||||||
&ast.AssignStmt{
|
return &ast.ExprStmt{X: &ast.CallExpr{
|
||||||
Lhs: []ast.Expr{testObj},
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("NotZero")},
|
||||||
Tok: token.DEFINE,
|
Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: obj, Sel: ast.NewIdent(field)}},
|
||||||
Rhs: []ast.Expr{&ast.CompositeLit{
|
}}
|
||||||
Type: ast.NewIdent(tbl.GoTypeName),
|
}
|
||||||
Elts: []ast.Expr{
|
|
||||||
&ast.KeyValueExpr{
|
stmts := []ast.Stmt{
|
||||||
Key: fieldName,
|
// item := Item{Description: "an item"}
|
||||||
Value: &ast.BasicLit{Kind: token.STRING, Value: description1},
|
&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},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TestDB.SaveItem(&item)
|
||||||
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Save" + tbl.GoTypeName)},
|
||||||
|
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
||||||
|
}},
|
||||||
|
|
||||||
|
// require.NotZero(t, item.ID)
|
||||||
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("require"), Sel: ast.NewIdent("NotZero")},
|
||||||
|
Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// After create: assert timestamps are set
|
||||||
|
if hasCreatedAt {
|
||||||
|
stmts = append(stmts, assertNotZero(testObj, "CreatedAt"))
|
||||||
|
}
|
||||||
|
if hasUpdatedAt {
|
||||||
|
stmts = append(stmts, assertNotZero(testObj, "UpdatedAt"))
|
||||||
|
}
|
||||||
|
|
||||||
|
stmts = append(stmts,
|
||||||
|
// item2 := Must(TestDB.GetItemByID(item.ID))
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{testObj2},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Get" + tbl.GoTypeName + "ByID")},
|
||||||
|
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
||||||
|
})},
|
||||||
|
},
|
||||||
|
|
||||||
|
// assert.Equal(t, "an item", item2.Description)
|
||||||
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
||||||
|
Args: []ast.Expr{
|
||||||
|
ast.NewIdent("t"),
|
||||||
|
&ast.BasicLit{Kind: token.STRING, Value: description1},
|
||||||
|
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||||
},
|
},
|
||||||
}},
|
}},
|
||||||
},
|
|
||||||
|
|
||||||
// TestDB.SaveItem(&item)
|
// item.Description = "a big item"
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&ast.AssignStmt{
|
||||||
Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName),
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: fieldName}},
|
||||||
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
Tok: token.ASSIGN,
|
||||||
}},
|
Rhs: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: description2}},
|
||||||
|
},
|
||||||
|
|
||||||
// require.NotZero(t, item.ID)
|
// TestDB.SaveItem(&item)
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
Fun: ast.NewIdent("require.NotZero"),
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Save" + tbl.GoTypeName)},
|
||||||
Args: []ast.Expr{ast.NewIdent("t"), &ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
||||||
}},
|
}},
|
||||||
|
)
|
||||||
|
|
||||||
// item2 := Must(TestDB.GetItemByID(item.ID))
|
stmts = append(stmts,
|
||||||
&ast.AssignStmt{
|
// item2 = Must(TestDB.GetItemByID(item.ID))
|
||||||
Lhs: []ast.Expr{testObj2},
|
&ast.AssignStmt{
|
||||||
Tok: token.DEFINE,
|
Lhs: []ast.Expr{testObj2},
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
Tok: token.ASSIGN,
|
||||||
Fun: ast.NewIdent("Must"),
|
Rhs: []ast.Expr{mustCall(&ast.CallExpr{
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Get" + tbl.GoTypeName + "ByID")},
|
||||||
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
|
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
||||||
|
})},
|
||||||
|
},
|
||||||
|
|
||||||
|
// assert.Equal(t, item.Description, item2.Description)
|
||||||
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("Equal")},
|
||||||
|
Args: []ast.Expr{
|
||||||
|
ast.NewIdent("t"),
|
||||||
|
&ast.SelectorExpr{X: testObj, Sel: fieldName},
|
||||||
|
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
|
||||||
|
// TestDB.DeleteItem(item)
|
||||||
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Delete" + tbl.GoTypeName)},
|
||||||
|
Args: []ast.Expr{testObj},
|
||||||
|
}},
|
||||||
|
|
||||||
|
// _, err := TestDB.GetItemByID(item.ID)
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{&ast.CallExpr{
|
||||||
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("TestDB"), Sel: ast.NewIdent("Get" + tbl.GoTypeName + "ByID")},
|
||||||
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
||||||
}},
|
}},
|
||||||
}},
|
|
||||||
},
|
|
||||||
|
|
||||||
// assert.Equal(t, "an item", item2.Description)
|
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("assert.Equal"),
|
|
||||||
Args: []ast.Expr{
|
|
||||||
ast.NewIdent("t"),
|
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: description1},
|
|
||||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
|
||||||
},
|
},
|
||||||
}},
|
|
||||||
|
|
||||||
// item.Description = "a big item"
|
// assert.ErrorIs(t, err, db.ErrNotInDB)
|
||||||
&ast.AssignStmt{
|
&ast.ExprStmt{X: &ast.CallExpr{
|
||||||
Lhs: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: fieldName}},
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("assert"), Sel: ast.NewIdent("ErrorIs")},
|
||||||
Tok: token.ASSIGN,
|
Args: []ast.Expr{
|
||||||
Rhs: []ast.Expr{&ast.BasicLit{Kind: token.STRING, Value: description2}},
|
ast.NewIdent("t"),
|
||||||
},
|
ast.NewIdent("err"),
|
||||||
|
ast.NewIdent("ErrNotInDB"),
|
||||||
// TestDB.SaveItem(&item)
|
},
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("TestDB.Save" + tbl.GoTypeName),
|
|
||||||
Args: []ast.Expr{&ast.UnaryExpr{Op: token.AND, X: testObj}},
|
|
||||||
}},
|
|
||||||
|
|
||||||
// item2 = Must(TestDB.GetItemByID(item.ID))
|
|
||||||
&ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{testObj2},
|
|
||||||
Tok: token.ASSIGN,
|
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("Must"),
|
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
|
|
||||||
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
|
||||||
}},
|
|
||||||
}},
|
}},
|
||||||
},
|
)
|
||||||
|
|
||||||
// assert.Equal(t, item.Description, item2.Description)
|
return stmts
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
}(),
|
||||||
Fun: ast.NewIdent("assert.Equal"),
|
|
||||||
Args: []ast.Expr{
|
|
||||||
ast.NewIdent("t"),
|
|
||||||
&ast.SelectorExpr{X: testObj, Sel: fieldName},
|
|
||||||
&ast.SelectorExpr{X: testObj2, Sel: fieldName},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
|
|
||||||
// TestDB.DeleteItem(item)
|
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("TestDB.Delete" + tbl.GoTypeName),
|
|
||||||
Args: []ast.Expr{testObj},
|
|
||||||
}},
|
|
||||||
|
|
||||||
// _, err := TestDB.GetItemByID(item.ID)
|
|
||||||
&ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
|
||||||
Tok: token.DEFINE,
|
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("TestDB.Get" + tbl.GoTypeName + "ByID"),
|
|
||||||
Args: []ast.Expr{&ast.SelectorExpr{X: testObj, Sel: ast.NewIdent("ID")}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
|
|
||||||
// assert.ErrorIs(t, err, db.ErrNotInDB)
|
|
||||||
&ast.ExprStmt{X: &ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("assert.ErrorIs"),
|
|
||||||
Args: []ast.Expr{
|
|
||||||
ast.NewIdent("t"),
|
|
||||||
ast.NewIdent("err"),
|
|
||||||
&ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("ErrNotInDB")},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testGetAll := &ast.FuncDecl{
|
testGetAll := &ast.FuncDecl{
|
||||||
Name: ast.NewIdent("TestGetAll" + inflection.Plural(tbl.GoTypeName)),
|
Name: ast.NewIdent("TestGetAll" + inflection.Plural(tbl.GoTypeName)),
|
||||||
Type: &ast.FuncType{Params: &ast.FieldList{List: []*ast.Field{
|
Type: testFuncType,
|
||||||
{Names: []*ast.Ident{ast.NewIdent("t")}, Type: &ast.StarExpr{X: ast.NewIdent("testing.T")}},
|
|
||||||
}}, Results: nil},
|
|
||||||
Body: &ast.BlockStmt{
|
Body: &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
@@ -237,17 +212,110 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: ast.NewIdent("TestDB"),
|
||||||
|
Sel: ast.NewIdent("Save" + tbl.GoTypeName),
|
||||||
|
},
|
||||||
|
Args: []ast.Expr{
|
||||||
|
&ast.UnaryExpr{
|
||||||
|
Op: token.AND,
|
||||||
|
X: ast.NewIdent(tbl.VarName),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// assertForeignKeyError(t, err, "QuotedPostID", post.QuotedPostID)
|
||||||
|
&ast.ExprStmt{
|
||||||
|
X: &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("AssertForeignKeyError"),
|
||||||
|
Args: []ast.Expr{
|
||||||
|
ast.NewIdent("t"),
|
||||||
|
ast.NewIdent("err"),
|
||||||
|
&ast.BasicLit{
|
||||||
|
Kind: token.STRING,
|
||||||
|
Value: fmt.Sprintf("%q", col.GoFieldName()),
|
||||||
|
},
|
||||||
|
&ast.SelectorExpr{
|
||||||
|
X: ast.NewIdent(tbl.VarName),
|
||||||
|
Sel: ast.NewIdent(col.GoFieldName()),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return stmts
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testList := []ast.Decl{
|
||||||
|
makeItemFunc,
|
||||||
|
testCreateUpdateDelete,
|
||||||
|
testGetAll,
|
||||||
|
}
|
||||||
|
if shouldIncludeTestFkCheck {
|
||||||
|
testList = append(testList, testFkChecking)
|
||||||
|
}
|
||||||
return &ast.File{
|
return &ast.File{
|
||||||
Name: ast.NewIdent(testpackageName),
|
Name: ast.NewIdent(testpackageName),
|
||||||
Decls: []ast.Decl{
|
Decls: append([]ast.Decl{
|
||||||
&ast.GenDecl{
|
&ast.GenDecl{
|
||||||
Tok: token.IMPORT,
|
Tok: token.IMPORT,
|
||||||
Specs: []ast.Spec{
|
Specs: []ast.Spec{
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"fmt"`}},
|
|
||||||
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"testing"`}},
|
&ast.ImportSpec{Path: &ast.BasicLit{Kind: token.STRING, Value: `"testing"`}},
|
||||||
&ast.ImportSpec{
|
&ast.ImportSpec{
|
||||||
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"`},
|
||||||
Name: ast.NewIdent("db"),
|
Name: ast.NewIdent("."),
|
||||||
},
|
},
|
||||||
&ast.ImportSpec{
|
&ast.ImportSpec{
|
||||||
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"`},
|
Path: &ast.BasicLit{Kind: token.STRING, Value: `"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"`},
|
||||||
@@ -261,17 +329,6 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
|
|||||||
&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"`}},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// var TestDB *DB
|
}, testList...),
|
||||||
testDBDecl,
|
|
||||||
|
|
||||||
// func init() { TestDB = MakeDB("tmp") }
|
|
||||||
initFuncDecl,
|
|
||||||
|
|
||||||
// func MakeDB(dbName string) *DB { db := Must(Create(fmt.Sprintf("file:%s?mode=memory&cache=shared", dbName))); return db }
|
|
||||||
makeDBHelperDecl,
|
|
||||||
|
|
||||||
testCreateUpdateDelete,
|
|
||||||
testGetAll,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||||
)
|
)
|
||||||
@@ -39,6 +40,11 @@ func InitPkg(opts PkgOpts) {
|
|||||||
PanicIf(os.WriteFile("pkg/db/schema.sql", Must(tpl.ReadFile("tpl/schema.sql")), 0o664))
|
PanicIf(os.WriteFile("pkg/db/schema.sql", Must(tpl.ReadFile("tpl/schema.sql")), 0o664))
|
||||||
PanicIf(os.WriteFile("pkg/db/db.go", Must(tpl.ReadFile("tpl/db.go.tpl")), 0o664))
|
PanicIf(os.WriteFile("pkg/db/db.go", Must(tpl.ReadFile("tpl/db.go.tpl")), 0o664))
|
||||||
|
|
||||||
|
dbTest := Must(os.Create("pkg/db/db_test.go"))
|
||||||
|
defer MustClose(dbTest)
|
||||||
|
t := Must(template.ParseFS(tpl, "tpl/db_test.go.tpl"))
|
||||||
|
PanicIf(t.Execute(dbTest, opts))
|
||||||
|
|
||||||
PanicIf(os.WriteFile("sample_data/mount.sh", Must(tpl.ReadFile("tpl/mount.sh")), 0o775))
|
PanicIf(os.WriteFile("sample_data/mount.sh", Must(tpl.ReadFile("tpl/mount.sh")), 0o775))
|
||||||
PanicIf(os.WriteFile("sample_data/reset.sh", Must(tpl.ReadFile("tpl/reset.sh")), 0o775))
|
PanicIf(os.WriteFile("sample_data/reset.sh", Must(tpl.ReadFile("tpl/reset.sh")), 0o775))
|
||||||
|
|
||||||
|
|||||||
19
pkg/codegen/tpl/db_test.go.tpl
Normal file
19
pkg/codegen/tpl/db_test.go.tpl
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package db_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||||
|
|
||||||
|
. "{{ .ModuleName }}/pkg/db"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TestDB *DB
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
TestDB = MakeDB("tmp")
|
||||||
|
}
|
||||||
|
func MakeDB(dbName string) *DB {
|
||||||
|
db := Must(Create(fmt.Sprintf("file:%s?mode=memory&cache=shared", dbName)))
|
||||||
|
return db
|
||||||
|
}
|
||||||
21
pkg/db/errors_test_helper.go
Normal file
21
pkg/db/errors_test_helper.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AssertForeignKeyError[T ForeignKey](t *testing.T, err error, field string, val T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var fkErr ForeignKeyError[T]
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, ErrForeignKeyViolation)
|
||||||
|
// ErrorAs produces terrible error messages if it's a ForeignKeyError with a different type
|
||||||
|
// parameter (i.e., if it was a different field that failed).
|
||||||
|
require.ErrorAs(t, err, &fkErr, "expected error field: %q", field)
|
||||||
|
assert.Equal(t, field, fkErr.Field)
|
||||||
|
assert.Equal(t, val, fkErr.FkValue)
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package flowutils
|
package flowutils
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
func PanicIf(err error) {
|
func PanicIf(err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -10,3 +12,7 @@ func Must[T any](val T, err error) T {
|
|||||||
PanicIf(err)
|
PanicIf(err)
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func MustClose(closer io.Closer) {
|
||||||
|
PanicIf(closer.Close())
|
||||||
|
}
|
||||||
|
|||||||
167
pkg/schema/migration_verification_test.go
Normal file
167
pkg/schema/migration_verification_test.go
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
package schema_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/db"
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
baseSchema = `
|
||||||
|
create table db_version (
|
||||||
|
version integer primary key
|
||||||
|
) strict, without rowid;
|
||||||
|
insert into db_version values(0);
|
||||||
|
|
||||||
|
create table t1 (
|
||||||
|
rowid integer primary key,
|
||||||
|
data1 integer not null,
|
||||||
|
data2 text not null
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
fullSchema = `
|
||||||
|
create table db_version (
|
||||||
|
version integer primary key
|
||||||
|
) strict, without rowid;
|
||||||
|
insert into db_version values(2);
|
||||||
|
|
||||||
|
create table t1 (
|
||||||
|
rowid integer primary key,
|
||||||
|
data1 integer not null,
|
||||||
|
data2 text not null,
|
||||||
|
data3 integer
|
||||||
|
);
|
||||||
|
create table t2 (
|
||||||
|
rowid integer primary key
|
||||||
|
);
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestVerifyCorrectMigration(t *testing.T) {
|
||||||
|
db1 := schema.InitDB(fullSchema)
|
||||||
|
db1Schema := schema.SchemaFromDB(db1)
|
||||||
|
|
||||||
|
t.Run("migrate in 1 step", func(t *testing.T) {
|
||||||
|
migration := `
|
||||||
|
create table t2 (
|
||||||
|
rowid integer primary key
|
||||||
|
);
|
||||||
|
alter table t1 add column data3 integer;
|
||||||
|
`
|
||||||
|
db2Config := db.Init(&baseSchema, &[]string{migration})
|
||||||
|
db2 := flowutils.Must(db2Config.Create(":memory:"))
|
||||||
|
require.NoError(t, db2Config.CheckAndUpdateVersion(db2))
|
||||||
|
db2Schema := schema.SchemaFromDB(db2)
|
||||||
|
|
||||||
|
if diff := deep.Equal(db1Schema, db2Schema); diff != nil {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("migrate in 2 steps", func(t *testing.T) {
|
||||||
|
migration1 := `
|
||||||
|
create table t2 (
|
||||||
|
rowid integer primary key
|
||||||
|
);
|
||||||
|
`
|
||||||
|
migration2 := `
|
||||||
|
alter table t1 add column data3 integer;
|
||||||
|
`
|
||||||
|
|
||||||
|
db2Config := db.Init(&baseSchema, &[]string{migration1, migration2})
|
||||||
|
db2 := flowutils.Must(db2Config.Create(":memory:"))
|
||||||
|
require.NoError(t, db2Config.CheckAndUpdateVersion(db2))
|
||||||
|
db2Schema := schema.SchemaFromDB(db2)
|
||||||
|
|
||||||
|
if diff := deep.Equal(db1Schema, db2Schema); diff != nil {
|
||||||
|
t.Error(diff)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIncorrectMigrations(t *testing.T) {
|
||||||
|
db1 := schema.InitDB(fullSchema)
|
||||||
|
db1Schema := schema.SchemaFromDB(db1)
|
||||||
|
|
||||||
|
t.Run("missing migration", func(t *testing.T) {
|
||||||
|
db2Config := db.Init(&baseSchema, &[]string{})
|
||||||
|
db2 := flowutils.Must(db2Config.Create(":memory:"))
|
||||||
|
require.NoError(t, db2Config.CheckAndUpdateVersion(db2))
|
||||||
|
db2Schema := schema.SchemaFromDB(db2)
|
||||||
|
|
||||||
|
// Missing a table
|
||||||
|
assert.Len(t, db1Schema.Tables, len(db2Schema.Tables)+1)
|
||||||
|
assert.Contains(t, db1Schema.Tables, "t2")
|
||||||
|
assert.NotContains(t, db2Schema.Tables, "t2")
|
||||||
|
|
||||||
|
// Missing the new column
|
||||||
|
assert.Len(t, db1Schema.Tables["t1"].Columns, len(db2Schema.Tables["t1"].Columns)+1)
|
||||||
|
assert.True(t, slices.ContainsFunc(db1Schema.Tables["t1"].Columns, func(c schema.Column) bool { return c.Name == "data3" }), "t2")
|
||||||
|
assert.False(t, slices.ContainsFunc(db2Schema.Tables["t1"].Columns, func(c schema.Column) bool { return c.Name == "data3" }), "t2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("incomplete migration", func(t *testing.T) {
|
||||||
|
db2Config := db.Init(&baseSchema, &[]string{`
|
||||||
|
create table t2 (
|
||||||
|
rowid integer primary key
|
||||||
|
);
|
||||||
|
`})
|
||||||
|
db2 := flowutils.Must(db2Config.Create(":memory:"))
|
||||||
|
require.NoError(t, db2Config.CheckAndUpdateVersion(db2))
|
||||||
|
db2Schema := schema.SchemaFromDB(db2)
|
||||||
|
|
||||||
|
// Has the new table
|
||||||
|
assert.Len(t, db1Schema.Tables, len(db2Schema.Tables))
|
||||||
|
assert.Contains(t, db1Schema.Tables, "t2")
|
||||||
|
assert.Contains(t, db2Schema.Tables, "t2")
|
||||||
|
|
||||||
|
// Still missing the new column
|
||||||
|
assert.Len(t, db1Schema.Tables["t1"].Columns, len(db2Schema.Tables["t1"].Columns)+1)
|
||||||
|
assert.True(t, slices.ContainsFunc(db1Schema.Tables["t1"].Columns, func(c schema.Column) bool { return c.Name == "data3" }), "t2")
|
||||||
|
assert.False(t, slices.ContainsFunc(db2Schema.Tables["t1"].Columns, func(c schema.Column) bool { return c.Name == "data3" }), "t2")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("incorrect migration (wrong data type)", func(t *testing.T) {
|
||||||
|
db2Config := db.Init(&baseSchema, &[]string{`
|
||||||
|
create table t2 (
|
||||||
|
rowid integer primary key
|
||||||
|
);
|
||||||
|
alter table t1 add column data3 text;
|
||||||
|
`})
|
||||||
|
db2 := flowutils.Must(db2Config.Create(":memory:"))
|
||||||
|
require.NoError(t, db2Config.CheckAndUpdateVersion(db2))
|
||||||
|
db2Schema := schema.SchemaFromDB(db2)
|
||||||
|
|
||||||
|
// Has the new table
|
||||||
|
assert.Len(t, db1Schema.Tables, len(db2Schema.Tables))
|
||||||
|
assert.Contains(t, db1Schema.Tables, "t2")
|
||||||
|
assert.Contains(t, db2Schema.Tables, "t2")
|
||||||
|
|
||||||
|
// Has the right column, but it's the wrong type
|
||||||
|
assert.Len(t, db1Schema.Tables["t1"].Columns, len(db2Schema.Tables["t1"].Columns))
|
||||||
|
col1 := db1Schema.Tables["t1"].Columns[slices.IndexFunc(
|
||||||
|
db1Schema.Tables["t1"].Columns,
|
||||||
|
func(c schema.Column) bool { return c.Name == "data3" },
|
||||||
|
)]
|
||||||
|
col2 := db2Schema.Tables["t1"].Columns[slices.IndexFunc(
|
||||||
|
db2Schema.Tables["t1"].Columns,
|
||||||
|
func(c schema.Column) bool { return c.Name == "data3" },
|
||||||
|
)]
|
||||||
|
|
||||||
|
assert.NotEqual(t, col1, col2)
|
||||||
|
assert.Equal(t, col1.Type, "integer") // Full schema has an integer column
|
||||||
|
assert.Equal(t, col2.Type, "text") // Migration incorrectly uses a text column
|
||||||
|
|
||||||
|
// Other than that they are equal
|
||||||
|
col2.Type = "integer"
|
||||||
|
assert.Equal(t, col1, col2)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -21,7 +21,6 @@ var create_views string
|
|||||||
func InitDB(sql_schema string) *sqlx.DB {
|
func InitDB(sql_schema string) *sqlx.DB {
|
||||||
db := sqlx.MustOpen("sqlite3", ":memory:")
|
db := sqlx.MustOpen("sqlite3", ":memory:")
|
||||||
db.MustExec(sql_schema)
|
db.MustExec(sql_schema)
|
||||||
db.MustExec(create_views)
|
|
||||||
return db
|
return db
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +35,7 @@ func SchemaFromSQLFile(filepath string) (Schema, error) {
|
|||||||
// SchemaFromDB takes a DB connection, checks its schema metadata tables, and returns a Schema.
|
// SchemaFromDB takes a DB connection, checks its schema metadata tables, and returns a Schema.
|
||||||
func SchemaFromDB(db *sqlx.DB) Schema {
|
func SchemaFromDB(db *sqlx.DB) Schema {
|
||||||
ret := Schema{Tables: map[string]Table{}, Indexes: map[string]Index{}}
|
ret := Schema{Tables: map[string]Table{}, Indexes: map[string]Index{}}
|
||||||
|
db.MustExec(create_views)
|
||||||
|
|
||||||
var tables []Table
|
var tables []Table
|
||||||
PanicIf(db.Select(&tables, `select name, table_type, is_strict, is_without_rowid from tables`))
|
PanicIf(db.Select(&tables, `select name, table_type, is_strict, is_without_rowid from tables`))
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||||
|
)
|
||||||
|
|
||||||
// Column represents a single column in a table.
|
// Column represents a single column in a table.
|
||||||
type Column struct {
|
type Column struct {
|
||||||
TableName string `db:"table_name"`
|
TableName string `db:"table_name"`
|
||||||
@@ -20,6 +27,43 @@ func (c Column) IsNullableForeignKey() bool {
|
|||||||
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) GoType() 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.
|
// Table is a single SQLite table.
|
||||||
type Table struct {
|
type Table struct {
|
||||||
TableName string `db:"name"`
|
TableName string `db:"name"`
|
||||||
@@ -40,6 +84,35 @@ type Table struct {
|
|||||||
GoTypeName string
|
GoTypeName string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PrimaryKeyColumns returns the ordered list of columns in this table's primary key.
|
||||||
|
// This can be useful for "without rowid" tables with composite primary keys.
|
||||||
|
//
|
||||||
|
// TODO: needs test
|
||||||
|
func (t Table) PrimaryKeyColumns() []Column {
|
||||||
|
pks := make([]Column, 0)
|
||||||
|
for _, c := range t.Columns {
|
||||||
|
if c.IsPrimaryKey {
|
||||||
|
pks = append(pks, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Slice(pks, func(i, j int) bool {
|
||||||
|
return pks[i].PrimaryKeyRank < pks[j].PrimaryKeyRank
|
||||||
|
})
|
||||||
|
return pks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
||||||
|
for _, c := range t.Columns {
|
||||||
|
if c.Name == "created_at" && c.Type == "integer" {
|
||||||
|
hasCreatedAt = true
|
||||||
|
}
|
||||||
|
if c.Name == "updated_at" && c.Type == "integer" {
|
||||||
|
hasUpdatedAt = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
type Index struct {
|
type Index struct {
|
||||||
Name string `db:"index_name"`
|
Name string `db:"index_name"`
|
||||||
TableName string `db:"table_name"`
|
TableName string `db:"table_name"`
|
||||||
|
|||||||
Reference in New Issue
Block a user