Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c56661560 | |||
| 3d357abb93 | |||
|
|
a3da7573c1 | ||
|
|
b96ab19bc2 | ||
|
|
cb8edd74c0 | ||
|
|
a0d0461f06 | ||
|
|
e85a68e69d | ||
|
|
eee6714918 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,5 @@
|
|||||||
sample_data/data
|
sample_data/data
|
||||||
|
|
||||||
|
# Legacy versions
|
||||||
|
.cmd-old/
|
||||||
|
pkg/.testapp
|
||||||
|
|||||||
14
doc/TODO.txt
14
doc/TODO.txt
@ -15,3 +15,17 @@ TODO: generator-foreign-keys
|
|||||||
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 `without rowid` tables properly
|
||||||
|
|
||||||
|
TODO: generated test file inclues global test DB setup, which is wrong
|
||||||
|
|||||||
@ -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:
|
||||||
@ -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,17 @@ 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)
|
||||||
) strict;
|
) strict;
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,14 @@ func GenerateIDType(table schema.Table) *ast.GenDecl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fkFieldName(col schema.Column) string {
|
||||||
|
if col.IsNonCodeTableForeignKey() {
|
||||||
|
return textutils.SnakeToCamel(strings.TrimSuffix(col.Name, "_id")) + "ID"
|
||||||
|
} else {
|
||||||
|
return textutils.SnakeToCamel(col.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateModelAST produces an AST for a struct type corresponding to the model.
|
// GenerateModelAST produces an AST for a struct type corresponding to the model.
|
||||||
// TODO: generate the right field types here based on column types.
|
// TODO: generate the right field types here based on column types.
|
||||||
func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
||||||
@ -37,9 +45,9 @@ 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(fkFieldName(col))},
|
||||||
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)},
|
||||||
})
|
})
|
||||||
@ -52,7 +60,7 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
|
|||||||
} else if strings.HasSuffix(col.Name, "_at") {
|
} else if strings.HasSuffix(col.Name, "_at") {
|
||||||
typeName = "Timestamp"
|
typeName = "Timestamp"
|
||||||
} else {
|
} else {
|
||||||
typeName = "int64"
|
typeName = "int"
|
||||||
}
|
}
|
||||||
case "text":
|
case "text":
|
||||||
typeName = "string"
|
typeName = "string"
|
||||||
@ -102,33 +110,227 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
updatePairs = append(updatePairs, col.Name+"="+val)
|
updatePairs = append(updatePairs, col.Name+"="+val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasFks := false
|
||||||
|
checkForeignKeyFailuresAssignment := &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 := fkFieldName(col)
|
||||||
|
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: &ast.SelectorExpr{X: ast.NewIdent("db"), Sel: ast.NewIdent("DB")}, 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
|
||||||
|
}(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
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 "))
|
||||||
|
|
||||||
funcBody := &ast.BlockStmt{
|
funcBody := &ast.BlockStmt{
|
||||||
List: []ast.Stmt{
|
List: func() []ast.Stmt {
|
||||||
&ast.IfStmt{
|
ret := []ast.Stmt{}
|
||||||
|
if hasFks {
|
||||||
|
ret = append(ret, checkForeignKeyFailuresAssignment)
|
||||||
|
}
|
||||||
|
// 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,
|
namedExecStmt := &ast.CallExpr{
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: ast.NewIdent("Must"),
|
|
||||||
Args: []ast.Expr{&ast.CallExpr{
|
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")},
|
Fun: &ast.SelectorExpr{X: ast.NewIdent("db.DB"), Sel: ast.NewIdent("NamedExec")},
|
||||||
Args: []ast.Expr{
|
Args: []ast.Expr{
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
&ast.BasicLit{Kind: token.STRING, Value: "`" + insertStmt + "`"},
|
||||||
ast.NewIdent(tbl.VarName),
|
ast.NewIdent(tbl.VarName),
|
||||||
},
|
},
|
||||||
}},
|
}
|
||||||
|
if !hasFks {
|
||||||
|
// No foreign key checking needed; just use `Must` for brevity
|
||||||
|
return []ast.Stmt{
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{&ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("Must"),
|
||||||
|
Args: []ast.Expr{namedExecStmt},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []ast.Stmt{
|
||||||
|
// result, err := db.DB.NamedExec(`...`, u)
|
||||||
|
&ast.AssignStmt{
|
||||||
|
Lhs: []ast.Expr{
|
||||||
|
ast.NewIdent("result"),
|
||||||
|
ast.NewIdent("err"),
|
||||||
|
},
|
||||||
|
Tok: token.DEFINE,
|
||||||
|
Rhs: []ast.Expr{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: &ast.IfStmt{
|
||||||
|
Cond: &ast.BinaryExpr{
|
||||||
|
X: ast.NewIdent("err"),
|
||||||
|
Op: token.NEQ,
|
||||||
|
Y: ast.NewIdent("nil"),
|
||||||
|
},
|
||||||
|
Body: &ast.BlockStmt{
|
||||||
|
List: []ast.Stmt{
|
||||||
|
&ast.ExprStmt{
|
||||||
|
X: &ast.CallExpr{
|
||||||
|
Fun: ast.NewIdent("panic"),
|
||||||
|
Args: []ast.Expr{ast.NewIdent("err")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}(),
|
||||||
&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,
|
||||||
@ -143,9 +345,10 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
},
|
),
|
||||||
},
|
},
|
||||||
Else: &ast.BlockStmt{
|
Else: &ast.BlockStmt{
|
||||||
|
// Do update
|
||||||
List: []ast.Stmt{
|
List: []ast.Stmt{
|
||||||
&ast.AssignStmt{
|
&ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("result")},
|
Lhs: []ast.Expr{ast.NewIdent("result")},
|
||||||
@ -175,8 +378,13 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
},
|
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{
|
||||||
@ -184,18 +392,25 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
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")}}}
|
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")}}}
|
||||||
@ -228,7 +443,7 @@ func GenerateGetItemByIDFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
|
|
||||||
funcDecl := &ast.FuncDecl{
|
funcDecl := &ast.FuncDecl{
|
||||||
Recv: recv,
|
Recv: recv,
|
||||||
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,
|
||||||
}
|
}
|
||||||
@ -258,12 +473,12 @@ func GenerateGetAllItemsFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
&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 + "`"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -351,8 +566,12 @@ 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 {
|
||||||
|
if col.IsNullableForeignKey() {
|
||||||
|
columns = append(columns, fmt.Sprintf("ifnull(%s, 0) %s", col.Name, col.Name))
|
||||||
|
} else {
|
||||||
columns = append(columns, col.Name)
|
columns = append(columns, col.Name)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Join with comma and space
|
// Join with comma and space
|
||||||
value := "`" + strings.Join(columns, ", ") + "`"
|
value := "`" + strings.Join(columns, ", ") + "`"
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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 schema
|
package schema
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
// 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 +22,10 @@ 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")
|
||||||
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user