5 Commits

Author SHA1 Message Date
616304c7dd ci: add support for manually triggering the build
All checks were successful
CI / build-docker (push) Successful in 5s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Successful in 4m14s
2026-07-12 14:05:05 -07:00
17fc8a68f6 codegen: wrap foreign key checks for nullable FKs in "if a.Val != 0 { ... }" 2026-07-12 13:34:21 -07:00
d572745613 codegen: don't skip created_at auto-timestamp for 'without rowid' tables 2026-07-12 13:20:35 -07:00
9bfb31798c codegen: don't auto-timestamp overwrite provided timestamps if there are ones 2026-07-07 12:15:51 -07:00
eafeb658bd codegen: fix invalid SQL query being generated for GetItemBy with multiple params 2026-06-24 13:59:13 -07:00
2 changed files with 109 additions and 66 deletions

View File

@@ -1,6 +1,6 @@
name: CI
on: [push]
on: [push, workflow_dispatch]
jobs:
# These steps build the `gas` docker image.

View File

@@ -161,79 +161,92 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
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},
ret = append(ret, func() ast.Stmt {
// Wrap nullable FKs in "if a.val != 0 { ... }"
wrap := func(input ast.Stmt) ast.Stmt {
if col.IsNullableForeignKey() {
return &ast.IfStmt{
Cond: &ast.BinaryExpr{X: structField, Op: token.NEQ, Y: &ast.BasicLit{Kind: token.INT, Value: "0"}},
Body: &ast.BlockStmt{List: []ast.Stmt{input}},
}
} else {
return input
}
}
if col.IsNonCodeTableForeignKey() {
// Real foreign key; look up referent by ID to see if it exists
return wrap(&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,
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.DEFINE,
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,
})
} else {
// Code table value. Query the table to see if it exists
return wrap(&ast.IfStmt{
Init: &ast.AssignStmt{
Lhs: []ast.Expr{ast.NewIdent("err")},
Tok: token.DEFINE,
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.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", 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.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
structField,
},
},
},
},
},
},
},
})
}
})
}
}())
}
// final return nil
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("nil")}})
@@ -386,6 +399,24 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
}
if tbl.IsWithoutRowid {
if hasCreatedAt {
// Auto-timestamps: created_at. Don't overwrite existing timestamps (e.g., data import / migrations)
ret = append(ret, &ast.IfStmt{
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")},
Sel: ast.NewIdent("IsZero"),
}},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
},
},
},
})
}
ret = append(ret, namedExecStmt(upsertStmt)...)
ret = append(ret, PanicIfRowsAffected(tbl))
} else {
@@ -403,10 +434,21 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
ret1 := []ast.Stmt{Comment("Do create")}
if hasCreatedAt {
// Auto-timestamps: created_at
ret1 = append(ret1, &ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
ret1 = append(ret1, &ast.IfStmt{
// Don't overwrite existing timestamps. This is useful for various reasons, e.g., data import / migrations
Cond: &ast.CallExpr{Fun: &ast.SelectorExpr{
X: &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")},
Sel: ast.NewIdent("IsZero"),
}},
Body: &ast.BlockStmt{
List: []ast.Stmt{
&ast.AssignStmt{
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
Tok: token.ASSIGN,
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
},
},
},
})
}
return append(ret1, namedExecStmt(insertStmt)...)
@@ -477,7 +519,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
for _, col := range cols {
funcParam := ast.NewIdent(col.LongGoVarName())
funcParams.List = append(funcParams.List, &ast.Field{Names: []*ast.Ident{funcParam}, Type: GoTypeForColumn(col)})
colNames = append(colNames, fmt.Sprintf("%s = :%s", col.Name, col.Name))
colNames = append(colNames, fmt.Sprintf("%s = ?", col.Name))
funcNameSuffix = append(funcNameSuffix, col.GoFieldName())
sqlParams = append(sqlParams, funcParam)
}
@@ -489,7 +531,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
Y: SQLFieldsConstIdent(tbl),
},
Op: token.ADD,
Y: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`\n\t from %s\n\t where %s = ?\n\t`", tbl.TableName, strings.Join(colNames, " and "))},
Y: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`\n\t from %s\n\t where %s\n\t`", tbl.TableName, strings.Join(colNames, " and "))},
}
return &ast.FuncDecl{
@@ -515,7 +557,8 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
&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")}}},
Args: []ast.Expr{ast.NewIdent("err"), &ast.SelectorExpr{X: ast.NewIdent("sql"), Sel: ast.NewIdent("ErrNoRows")}},
},
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.GoTypeName)}, ast.NewIdent("ErrNotInDB")}}}},
},
&ast.ReturnStmt{},