5 Commits

Author SHA1 Message Date
wispem-wantex
b96ab19bc2 refactor: create helper functions in model generator
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 2m25s
2026-01-31 18:25:53 -08:00
wispem-wantex
cb8edd74c0 codegen: fix whitespace error in generated SQL 2026-01-29 10:54:58 -08:00
wispem-wantex
a0d0461f06 codegen: handle nullable foreign keys using 'ifnull' and add a test case for them
All checks were successful
CI / build-docker (push) Successful in 18s
CI / build-docker-bootstrap (push) Has been skipped
CI / release-test (push) Successful in 1m2s
2026-01-26 16:00:19 -08:00
wispem-wantex
e85a68e69d devcontainer: make it work on mac
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 2m17s
2026-01-26 14:54:59 -08:00
wispem-wantex
eee6714918 codegen: style fixes ('int64' -> 'int', 'SELECT' -> 'select') 2026-01-26 14:53:04 -08:00
6 changed files with 53 additions and 11 deletions

View File

@@ -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

View File

@@ -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.
# #

View File

@@ -0,0 +1,3 @@
root:x:0:
nogroup:x:65533:
nobody:x:65534:

View File

@@ -31,9 +31,12 @@ cd $test_project
# 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

View File

@@ -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"
@@ -191,11 +199,13 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
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 +238,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 +268,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 +361,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, ", ") + "`"

View File

@@ -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"`