Compare commits

...

2 Commits

Author SHA1 Message Date
fcf266eb1d tests: add integration test for blob fields
All checks were successful
CI / build-docker (push) Successful in 6s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Successful in 2m24s
2026-03-03 17:55:50 -08:00
29787b5521 fix: make codegen not fail for "blob" columns
- was previously using "[]byte" as a single `ast.NewIdent`, which made comment+blank-line-insertion reparsing fail
2026-03-03 17:53:47 -08:00
5 changed files with 89 additions and 52 deletions

View File

@ -41,6 +41,7 @@ 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), flavor integer references item_flavor(rowid),
data blob not null,
thing text not null unique, thing text not null unique,
created_at integer not null, created_at integer not null,
updated_at integer not null updated_at integer not null

View File

@ -76,7 +76,12 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
return fmt.Errorf("re-parsing pretty-print: %w", err) return fmt.Errorf("re-parsing pretty-print: %w", err)
} }
// Convert the tree-of-nodes into a slice-of-nodes // Parallel walk: apply TrailingComments from the side map.
// Both trees have identical structure (the reparse is just a positioned copy),
// so ast.Inspect visits nodes in the same order. We skip comment nodes to
// avoid mismatches from Doc fields.
if len(TrailingComments) > 0 {
// Helper: convert the tree-of-nodes into a slice-of-nodes
collectNodes := func(node ast.Node) []ast.Node { collectNodes := func(node ast.Node) []ast.Node {
var nodes []ast.Node var nodes []ast.Node
ast.Inspect(node, func(n ast.Node) bool { ast.Inspect(node, func(n ast.Node) bool {
@ -91,13 +96,15 @@ func FprintWithComments(w io.Writer, file *ast.File) error {
return nodes 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) origNodes := collectNodes(file)
reparsedNodes := collectNodes(parsed) reparsedNodes := collectNodes(parsed)
if len(origNodes) != len(reparsedNodes) {
panic(fmt.Sprintf(
"origNodes: %d; reparsedNodes: %d. The AST generator is likely generating an invalid AST",
len(origNodes), len(reparsedNodes),
))
}
for i, orig := range origNodes { for i, orig := range origNodes {
text, isOk := TrailingComments[orig] text, isOk := TrailingComments[orig]
if !isOk { if !isOk {

View File

@ -27,6 +27,33 @@ func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields") return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
} }
// GoTypeForColumn returns a type expression for this column.
//
// For most columns this isjust its mapped name as a `ast.NewIdent`, but for "blob" it needs
// a slice expression (`[]byte`).
func GoTypeForColumn(c schema.Column) ast.Expr {
if c.IsNonCodeTableForeignKey() {
return ast.NewIdent(schema.TypenameFromTablename(c.ForeignKeyTargetTable) + "ID")
}
switch c.Type {
case "integer", "int":
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
return ast.NewIdent("bool")
} else if strings.HasSuffix(c.Name, "_at") {
return ast.NewIdent("Timestamp")
}
return ast.NewIdent("int")
case "text":
return ast.NewIdent("string")
case "real":
return ast.NewIdent("float32")
case "blob":
return &ast.ArrayType{Elt: ast.NewIdent("byte")}
default:
panic("Unrecognized sqlite column type: " + c.Type)
}
}
// --------------- // ---------------
// Generators // Generators
// --------------- // ---------------
@ -63,10 +90,9 @@ func GenerateModelAST(table schema.Table) *ast.GenDecl {
Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)}, Tag: &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("`db:\"%s\" json:\"%s\"`", col.Name, col.Name)},
}) })
} else { } else {
typeName := col.GoTypeName()
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: GoTypeForColumn(col),
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)},
}) })
} }
@ -183,7 +209,7 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
Fun: ast.NewIdent("NewForeignKeyError"), Fun: ast.NewIdent("NewForeignKeyError"),
Args: []ast.Expr{ Args: []ast.Expr{
&ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)}, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", structFieldName)},
ast.NewIdent(fmt.Sprintf("%q", col.ForeignKeyTargetTable)), &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", col.ForeignKeyTargetTable)},
structField, structField,
}, },
}, },
@ -478,7 +504,7 @@ func GenerateGetItemByUniqColFunc(tbl schema.Table, col schema.Column) *ast.Func
Name: ast.NewIdent("Get" + schema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName()), Name: ast.NewIdent("Get" + schema.TypenameFromTablename(tbl.TableName) + "By" + col.GoFieldName()),
Type: &ast.FuncType{ Type: &ast.FuncType{
Params: &ast.FieldList{List: []*ast.Field{ Params: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{param}, Type: ast.NewIdent(col.GoTypeName())}, {Names: []*ast.Ident{param}, Type: GoTypeForColumn(col)},
}}, }},
Results: &ast.FieldList{List: []*ast.Field{ Results: &ast.FieldList{List: []*ast.Field{
{Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)}, {Names: []*ast.Ident{ast.NewIdent("ret")}, Type: ast.NewIdent(tbl.GoTypeName)},

View File

@ -32,6 +32,24 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
Results: []ast.Expr{ Results: []ast.Expr{
&ast.CompositeLit{ &ast.CompositeLit{
Type: ast.NewIdent(tbl.GoTypeName), Type: ast.NewIdent(tbl.GoTypeName),
Elts: []ast.Expr{
&ast.KeyValueExpr{
Key: ast.NewIdent("Data"),
Value: &ast.CompositeLit{
Type: &ast.ArrayType{
Elt: ast.NewIdent("byte"),
},
Elts: []ast.Expr{},
},
},
&ast.KeyValueExpr{
Key: ast.NewIdent("Description"),
Value: &ast.BasicLit{
Kind: token.STRING,
Value: `""`,
},
},
},
}, },
}, },
}, },
@ -41,7 +59,7 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
testObj := ast.NewIdent("item") testObj := ast.NewIdent("item")
testObj2 := ast.NewIdent("item2") testObj2 := ast.NewIdent("item2")
fieldName := ast.NewIdent("Description") fieldName := ast.NewIdent("Description") // TODO
description1 := `"an item"` description1 := `"an item"`
description2 := `"a big item"` description2 := `"a big item"`
testDB := ast.NewIdent("TestDB") testDB := ast.NewIdent("TestDB")
@ -72,19 +90,27 @@ func GenerateModelTestAST(tbl pkgschema.Table, schema pkgschema.Schema, gomodNam
stmts := []ast.Stmt{ stmts := []ast.Stmt{
Comment("Create"), Comment("Create"),
// item := Item{Description: "an item"} // item := MakeItem()
&ast.AssignStmt{ &ast.AssignStmt{
Lhs: []ast.Expr{testObj}, Lhs: []ast.Expr{testObj},
Tok: token.DEFINE, Tok: token.DEFINE,
Rhs: []ast.Expr{&ast.CompositeLit{ Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("MakeItem"), Args: nil}},
Type: ast.NewIdent(tbl.GoTypeName), },
Elts: []ast.Expr{ // item.Description = "an item"
&ast.KeyValueExpr{ &ast.AssignStmt{
Key: fieldName, Lhs: []ast.Expr{
Value: &ast.BasicLit{Kind: token.STRING, Value: description1}, &ast.SelectorExpr{
X: ast.NewIdent("item"),
Sel: ast.NewIdent("Description"),
},
},
Tok: token.ASSIGN,
Rhs: []ast.Expr{
&ast.BasicLit{
Kind: token.STRING,
Value: `"an item"`,
}, },
}, },
}},
}, },
// TestDB.SaveItem(&item) // TestDB.SaveItem(&item)

View File

@ -57,29 +57,6 @@ func (c Column) GoVarName() string {
return strings.ToLower(fieldname)[0:1] + fieldname[1:] return strings.ToLower(fieldname)[0:1] + fieldname[1:]
} }
func (c Column) GoTypeName() string {
if c.IsNonCodeTableForeignKey() {
return TypenameFromTablename(c.ForeignKeyTargetTable) + "ID"
}
switch c.Type {
case "integer", "int":
if strings.HasPrefix(c.Name, "is_") || strings.HasPrefix(c.Name, "has_") {
return "bool"
} else if strings.HasSuffix(c.Name, "_at") {
return "Timestamp"
}
return "int"
case "text":
return "string"
case "real":
return "float32"
case "blob":
return "[]byte"
default:
panic("Unrecognized sqlite column type: " + c.Type)
}
}
// Table is a single SQLite table. // Table is a single SQLite table.
type Table struct { type Table struct {
TableName string `db:"name"` TableName string `db:"name"`