Compare commits
No commits in common. "wispem/codetables" and "master" have entirely different histories.
wispem/cod
...
master
@ -22,7 +22,6 @@ func main() {
|
|||||||
root_cmd.AddCommand(sqlite_lint)
|
root_cmd.AddCommand(sqlite_lint)
|
||||||
root_cmd.AddCommand(cmd_init)
|
root_cmd.AddCommand(cmd_init)
|
||||||
root_cmd.AddCommand(generate_model)
|
root_cmd.AddCommand(generate_model)
|
||||||
root_cmd.AddCommand(generate_codetable_type)
|
|
||||||
if err := root_cmd.Execute(); err != nil {
|
if err := root_cmd.Execute(); err != nil {
|
||||||
fmt.Println(RED + err.Error() + RESET)
|
fmt.Println(RED + err.Error() + RESET)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@ -1,55 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/codegen/modelgenerate"
|
|
||||||
. "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var generate_codetable_type = &cobra.Command{
|
|
||||||
Use: "generate_codetable <table_name>",
|
|
||||||
Short: "Generate a code-table enum type",
|
|
||||||
|
|
||||||
Args: cobra.ExactArgs(1),
|
|
||||||
|
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
|
||||||
path := Must(cmd.Flags().GetString("schema"))
|
|
||||||
sql, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reading path %s: %w", path, err)
|
|
||||||
}
|
|
||||||
db := schema.InitDB(string(sql))
|
|
||||||
schema := schema.SchemaFromDB(db)
|
|
||||||
table, isOk := schema.Tables[args[0]]
|
|
||||||
if !isOk {
|
|
||||||
return ErrNoSuchTable
|
|
||||||
}
|
|
||||||
|
|
||||||
vals := table.GetCodeTableValues(db)
|
|
||||||
decls := []ast.Decl{
|
|
||||||
modelgenerate.GenerateCodetableType(table),
|
|
||||||
modelgenerate.GenerateCodetableEnum(table, vals),
|
|
||||||
modelgenerate.GenerateCodetableStringerFunc(table, vals),
|
|
||||||
}
|
|
||||||
|
|
||||||
file := &ast.File{
|
|
||||||
Name: ast.NewIdent("db"), // TODO: parameterize
|
|
||||||
Decls: decls,
|
|
||||||
}
|
|
||||||
|
|
||||||
PanicIf(modelgenerate.FprintWithComments(os.Stdout, file))
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// DUPE: generate-flags
|
|
||||||
func init() {
|
|
||||||
generate_codetable_type.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
|
||||||
generate_codetable_type.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
|
||||||
generate_codetable_type.Flags().Bool("test", false, "Generate test file instead of regular file")
|
|
||||||
}
|
|
||||||
@ -101,7 +101,6 @@ var generate_model = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// DUPE: generate-flags
|
|
||||||
func init() {
|
func init() {
|
||||||
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
generate_model.Flags().String("schema", "pkg/db/schema.sql", "Path to SQL schema file")
|
||||||
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
generate_model.Flags().String("modname", "mymodule", "Name of project's Go module (TODO: detect automatically)")
|
||||||
|
|||||||
@ -1,111 +0,0 @@
|
|||||||
package modelgenerate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/token"
|
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/schema"
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GenerateCodetableType(table schema.Table) *ast.GenDecl {
|
|
||||||
return &ast.GenDecl{
|
|
||||||
Tok: token.TYPE,
|
|
||||||
Specs: []ast.Spec{&ast.TypeSpec{Name: &ast.Ident{Name: table.GoTypeName}, Type: &ast.Ident{Name: "int"}}},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateCodetableEnum(table schema.Table, vals []string) *ast.GenDecl {
|
|
||||||
getConstName := func(s string) string {
|
|
||||||
return table.GoTypeName + textutils.KebabToPascal(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
constSpecs := []ast.Spec{}
|
|
||||||
for i, val := range vals {
|
|
||||||
spec := &ast.ValueSpec{
|
|
||||||
Names: []*ast.Ident{{Name: getConstName(val)}},
|
|
||||||
}
|
|
||||||
// Only the first one needs `iota`
|
|
||||||
if i == 0 {
|
|
||||||
spec.Type = &ast.Ident{Name: table.GoTypeName}
|
|
||||||
spec.Values = []ast.Expr{&ast.BinaryExpr{
|
|
||||||
X: &ast.Ident{Name: "iota"},
|
|
||||||
Op: token.ADD,
|
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
constSpecs = append(constSpecs, spec)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ast.GenDecl{Tok: token.CONST, Specs: constSpecs}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateCodetableStringerFunc implements the `Stringer` interface by defining a `String() string` function.
|
|
||||||
func GenerateCodetableStringerFunc(table schema.Table, vals []string) *ast.FuncDecl {
|
|
||||||
objIdent := ast.NewIdent(table.VarName)
|
|
||||||
return &ast.FuncDecl{
|
|
||||||
Recv: &ast.FieldList{List: []*ast.Field{{Names: []*ast.Ident{objIdent}, Type: &ast.Ident{Name: table.GoTypeName}}}},
|
|
||||||
Name: &ast.Ident{Name: "String"},
|
|
||||||
Type: &ast.FuncType{Params: &ast.FieldList{}, Results: &ast.FieldList{List: []*ast.Field{{Type: &ast.Ident{Name: "string"}}}}},
|
|
||||||
Body: &ast.BlockStmt{
|
|
||||||
List: []ast.Stmt{
|
|
||||||
// names := []string{ ... }
|
|
||||||
&ast.AssignStmt{
|
|
||||||
Lhs: []ast.Expr{&ast.Ident{Name: "names"}},
|
|
||||||
Tok: token.DEFINE,
|
|
||||||
Rhs: []ast.Expr{
|
|
||||||
&ast.CompositeLit{
|
|
||||||
Type: &ast.ArrayType{Elt: &ast.Ident{Name: "string"}},
|
|
||||||
Elts: func() []ast.Expr {
|
|
||||||
ret := []ast.Expr{
|
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: `""`},
|
|
||||||
}
|
|
||||||
for _, val := range vals {
|
|
||||||
ret = append(ret, &ast.BasicLit{Kind: token.STRING, Value: fmt.Sprintf("%q", val)})
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
// if int(c) < 1 || int(c) >= len(names) { return "invalid" }
|
|
||||||
&ast.IfStmt{
|
|
||||||
Cond: &ast.BinaryExpr{
|
|
||||||
X: &ast.BinaryExpr{
|
|
||||||
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
|
||||||
Op: token.LSS,
|
|
||||||
Y: &ast.BasicLit{Kind: token.INT, Value: "1"},
|
|
||||||
},
|
|
||||||
Op: token.LOR,
|
|
||||||
Y: &ast.BinaryExpr{
|
|
||||||
X: &ast.CallExpr{Fun: &ast.Ident{Name: "int"}, Args: []ast.Expr{objIdent}},
|
|
||||||
Op: token.GEQ,
|
|
||||||
Y: &ast.CallExpr{Fun: &ast.Ident{Name: "len"}, Args: []ast.Expr{&ast.Ident{Name: "names"}}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Body: &ast.BlockStmt{
|
|
||||||
List: []ast.Stmt{
|
|
||||||
&ast.ReturnStmt{Results: []ast.Expr{
|
|
||||||
&ast.CallExpr{Fun: &ast.SelectorExpr{X: ast.NewIdent("fmt"), Sel: ast.NewIdent("Sprintf")}, Args: []ast.Expr{
|
|
||||||
&ast.BasicLit{Kind: token.STRING, Value: `"<%d=invalid>"`},
|
|
||||||
objIdent,
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// return names[c]
|
|
||||||
&ast.ReturnStmt{
|
|
||||||
Results: []ast.Expr{
|
|
||||||
&ast.IndexExpr{
|
|
||||||
X: &ast.Ident{Name: "names"},
|
|
||||||
Index: objIdent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Column represents a single column in a table.
|
|
||||||
type Column struct {
|
|
||||||
// TableName is the name of the SQLite table this column belongs to.
|
|
||||||
TableName string `db:"table_name"`
|
|
||||||
|
|
||||||
// Name is the SQLite column name.
|
|
||||||
Name string `db:"column_name"`
|
|
||||||
|
|
||||||
// Type is the SQLite type this column contains.
|
|
||||||
Type string `db:"column_type"`
|
|
||||||
|
|
||||||
IsNotNull bool `db:"notnull"`
|
|
||||||
HasDefaultValue bool `db:"has_default_value"`
|
|
||||||
DefaultValue string `db:"dflt_value"`
|
|
||||||
IsPrimaryKey bool `db:"is_primary_key"`
|
|
||||||
PrimaryKeyRank uint `db:"primary_key_rank"`
|
|
||||||
IsForeignKey bool `db:"is_foreign_key"`
|
|
||||||
ForeignKeyTargetTable string `db:"fk_target_table"`
|
|
||||||
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsNullableForeignKey is a helper function.
|
|
||||||
func (c Column) IsNullableForeignKey() bool {
|
|
||||||
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Column) IsNonCodeTableForeignKey() bool {
|
|
||||||
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c Column) GoFieldName() string {
|
|
||||||
if c.Name == "rowid" {
|
|
||||||
return "ID"
|
|
||||||
}
|
|
||||||
if c.IsNonCodeTableForeignKey() {
|
|
||||||
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
|
||||||
}
|
|
||||||
return textutils.SnakeToCamel(c.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
|
||||||
func (c Column) GoVarName() string {
|
|
||||||
if c.Name == "rowid" {
|
|
||||||
return strings.ToLower(c.TableName)[0:1] + "ID"
|
|
||||||
// TODO: Or should it just be "id"??
|
|
||||||
}
|
|
||||||
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
|
||||||
if c.IsNonCodeTableForeignKey() {
|
|
||||||
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, just use the whole name
|
|
||||||
return c.LongGoVarName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
|
||||||
func (c Column) LongGoVarName() string {
|
|
||||||
return textutils.CamelToPascal(c.GoFieldName())
|
|
||||||
}
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
type Index struct {
|
|
||||||
Name string `db:"index_name"`
|
|
||||||
TableName string `db:"table_name"`
|
|
||||||
Columns []string
|
|
||||||
IsUnique bool `db:"is_unique"`
|
|
||||||
// TODO: `where ...` for partial indexes
|
|
||||||
// TODO: identify columns that are expressions
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package schema
|
|
||||||
|
|
||||||
type Schema struct {
|
|
||||||
Tables map[string]Table
|
|
||||||
Indexes map[string]Index
|
|
||||||
}
|
|
||||||
@ -1,14 +1,66 @@
|
|||||||
package schema
|
package schema
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
|
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Column represents a single column in a table.
|
||||||
|
type Column struct {
|
||||||
|
TableName string `db:"table_name"`
|
||||||
|
Name string `db:"column_name"`
|
||||||
|
Type string `db:"column_type"`
|
||||||
|
IsNotNull bool `db:"notnull"`
|
||||||
|
HasDefaultValue bool `db:"has_default_value"`
|
||||||
|
DefaultValue string `db:"dflt_value"`
|
||||||
|
IsPrimaryKey bool `db:"is_primary_key"`
|
||||||
|
PrimaryKeyRank uint `db:"primary_key_rank"`
|
||||||
|
IsForeignKey bool `db:"is_foreign_key"`
|
||||||
|
ForeignKeyTargetTable string `db:"fk_target_table"`
|
||||||
|
ForeignKeyTargetColumn string `db:"fk_target_column"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNullableForeignKey is a helper function.
|
||||||
|
func (c Column) IsNullableForeignKey() bool {
|
||||||
|
return !c.IsNotNull && !c.IsPrimaryKey && c.IsForeignKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) IsNonCodeTableForeignKey() bool {
|
||||||
|
return c.IsForeignKey && strings.HasSuffix(c.Name, "_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Column) GoFieldName() string {
|
||||||
|
if c.Name == "rowid" {
|
||||||
|
return "ID"
|
||||||
|
}
|
||||||
|
if c.IsNonCodeTableForeignKey() {
|
||||||
|
return textutils.SnakeToCamel(strings.TrimSuffix(c.Name, "_id")) + "ID"
|
||||||
|
}
|
||||||
|
return textutils.SnakeToCamel(c.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoVarName returns the name of a local variable for this column, e.g., when used as a function parameter.
|
||||||
|
func (c Column) GoVarName() string {
|
||||||
|
if c.Name == "rowid" {
|
||||||
|
return strings.ToLower(c.TableName)[0:1] + "ID"
|
||||||
|
// TODO: Or should it just be "id"??
|
||||||
|
}
|
||||||
|
// For foreign keys, use first letter of the target type and "ID". "UserID" => "uID"
|
||||||
|
if c.IsNonCodeTableForeignKey() {
|
||||||
|
return strings.ToLower(c.ForeignKeyTargetTable)[0:1] + "ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, just use the whole name
|
||||||
|
return c.LongGoVarName()
|
||||||
|
}
|
||||||
|
|
||||||
|
// LongGoVarName returns a lowercased version of the field name (Pascal => Camel).
|
||||||
|
func (c Column) LongGoVarName() string {
|
||||||
|
return textutils.CamelToPascal(c.GoFieldName())
|
||||||
|
}
|
||||||
|
|
||||||
// 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"`
|
||||||
@ -67,10 +119,16 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Table) GetCodeTableValues(db *sqlx.DB) (ret []string) {
|
type Index struct {
|
||||||
if !slices.ContainsFunc(t.Columns, func(c Column) bool { return c.Name == "name" }) {
|
Name string `db:"index_name"`
|
||||||
panic("not a code table")
|
TableName string `db:"table_name"`
|
||||||
|
Columns []string
|
||||||
|
IsUnique bool `db:"is_unique"`
|
||||||
|
// TODO: `where ...` for partial indexes
|
||||||
|
// TODO: identify columns that are expressions
|
||||||
}
|
}
|
||||||
flowutils.PanicIf(db.Select(&ret, fmt.Sprintf("select name from %s", t.TableName)))
|
|
||||||
return
|
type Schema struct {
|
||||||
|
Tables map[string]Table
|
||||||
|
Indexes map[string]Index
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,16 +10,6 @@ func SnakeToCamel(s string) string {
|
|||||||
return strings.Join(parts, "")
|
return strings.Join(parts, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func KebabToPascal(s string) string {
|
|
||||||
parts := strings.Split(s, "-")
|
|
||||||
for i, part := range parts {
|
|
||||||
if len(part) > 0 {
|
|
||||||
parts[i] = strings.ToUpper(part[:1]) + part[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.Join(parts, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func CamelToPascal(s string) string {
|
func CamelToPascal(s string) string {
|
||||||
return strings.ToLower(s)[0:1] + s[1:]
|
return strings.ToLower(s)[0:1] + s[1:]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
create table item_types (
|
|
||||||
rowid integer primary key,
|
|
||||||
name text not null unique
|
|
||||||
) strict;
|
|
||||||
insert into item_types(rowid, name) values
|
|
||||||
(1, 'first-type'),
|
|
||||||
(2, 'second-type'),
|
|
||||||
(3, 'third-type');
|
|
||||||
Loading…
x
Reference in New Issue
Block a user