Compare commits

..

2 Commits

Author SHA1 Message Date
1c56661560 codegen: add foreign key error check test
All checks were successful
CI / build-docker (push) Successful in 4s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Successful in 17s
2026-02-01 08:04:12 -08:00
3d357abb93 gitignore: ignore temporary / legacy files 2026-01-31 20:36:57 -08:00
5 changed files with 176 additions and 17 deletions

4
.gitignore vendored
View File

@ -1 +1,5 @@
sample_data/data sample_data/data
# Legacy versions
.cmd-old/
pkg/.testapp

View File

@ -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,6 +30,9 @@ 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 item_flavor (rowid integer primary key, name text not null) strict;

View File

@ -214,7 +214,7 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
&ast.CallExpr{ &ast.CallExpr{
Fun: ast.NewIdent("NewForeignKeyError"), Fun: ast.NewIdent("NewForeignKeyError"),
Args: []ast.Expr{ Args: []ast.Expr{
ast.NewIdent(`"Type"`), &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)), ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)),
structField, structField,
}, },

View File

@ -83,6 +83,30 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
}, },
} }
// func MakeItem() Item { return Item{} }
makeItemFunc := &ast.FuncDecl{
Name: ast.NewIdent("Make" + tbl.GoTypeName),
Type: &ast.FuncType{
Params: &ast.FieldList{},
Results: &ast.FieldList{
List: []*ast.Field{
{Type: ast.NewIdent(tbl.GoTypeName)},
},
},
},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.ReturnStmt{
Results: []ast.Expr{
&ast.CompositeLit{
Type: ast.NewIdent(tbl.GoTypeName),
},
},
},
},
},
}
testObj := ast.NewIdent("item") testObj := ast.NewIdent("item")
testObj2 := ast.NewIdent("item2") testObj2 := ast.NewIdent("item2")
fieldName := ast.NewIdent("Description") fieldName := ast.NewIdent("Description")
@ -209,7 +233,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
Args: []ast.Expr{ Args: []ast.Expr{
ast.NewIdent("t"), ast.NewIdent("t"),
ast.NewIdent("err"), ast.NewIdent("err"),
&ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("ErrNotInDB")}, ast.NewIdent("ErrNotInDB"),
}, },
}}, }},
}, },
@ -237,9 +261,126 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
}, },
} }
shouldIncludeTestFkCheck := false
testFkChecking := &ast.FuncDecl{
Name: ast.NewIdent("Test" + tbl.GoTypeName + "FkChecking"),
Type: &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"),
},
},
},
},
},
},
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(fkFieldName(col)),
},
},
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", fkFieldName(col)),
},
&ast.SelectorExpr{
X: ast.NewIdent(tbl.VarName),
Sel: ast.NewIdent(fkFieldName(col)),
},
},
},
},
}...)
}
}
return stmts
}(),
},
}
testList := []ast.Decl{
// var TestDB *DB
testDBDecl,
makeItemFunc,
// 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,
}
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{
@ -247,7 +388,7 @@ func GenerateModelTestAST(tbl schema.Table, gomodName string) *ast.File {
&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 +402,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,
},
} }
} }

View 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)
}