Compare commits
7 Commits
dbf14e23b6
...
v0.0.3
| Author | SHA1 | Date | |
|---|---|---|---|
| a101c7531b | |||
| 9c31266f59 | |||
| 616304c7dd | |||
| 17fc8a68f6 | |||
| d572745613 | |||
| 9bfb31798c | |||
| eafeb658bd |
@@ -1,6 +1,6 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
on: [push]
|
on: [push, workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# These steps build the `gas` docker image.
|
# These steps build the `gas` docker image.
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
func SQLFieldsConstIdent(tbl schema.Table) *ast.Ident {
|
||||||
return ast.NewIdent(strings.ToLower(tbl.GoTypeName) + "SQLFields")
|
return ast.NewIdent(strings.ToLower(tbl.GoTypeName[:1]) + tbl.GoTypeName[1:] + "SQLFields")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GoTypeForColumn returns a type expression for this column.
|
// GoTypeForColumn returns a type expression for this column.
|
||||||
@@ -161,9 +161,21 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
|||||||
structFieldName := col.GoFieldName()
|
structFieldName := col.GoFieldName()
|
||||||
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
structField := &ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent(structFieldName)}
|
||||||
|
|
||||||
|
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() {
|
if col.IsNonCodeTableForeignKey() {
|
||||||
// Real foreign key; look up referent by ID to see if it exists
|
// Real foreign key; look up referent by ID to see if it exists
|
||||||
ret = append(ret, &ast.IfStmt{
|
return wrap(&ast.IfStmt{
|
||||||
Init: &ast.AssignStmt{
|
Init: &ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
Lhs: []ast.Expr{ast.NewIdent("_"), ast.NewIdent("err")},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
@@ -197,7 +209,7 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Code table value. Query the table to see if it exists
|
// Code table value. Query the table to see if it exists
|
||||||
ret = append(ret, &ast.IfStmt{
|
return wrap(&ast.IfStmt{
|
||||||
Init: &ast.AssignStmt{
|
Init: &ast.AssignStmt{
|
||||||
Lhs: []ast.Expr{ast.NewIdent("err")},
|
Lhs: []ast.Expr{ast.NewIdent("err")},
|
||||||
Tok: token.DEFINE,
|
Tok: token.DEFINE,
|
||||||
@@ -234,6 +246,7 @@ func buildFKCheckLambda(tbl schema.Table) (*ast.AssignStmt, bool) {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
}())
|
||||||
}
|
}
|
||||||
// final return nil
|
// final return nil
|
||||||
ret = append(ret, &ast.ReturnStmt{Results: []ast.Expr{ast.NewIdent("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 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, namedExecStmt(upsertStmt)...)
|
||||||
ret = append(ret, PanicIfRowsAffected(tbl))
|
ret = append(ret, PanicIfRowsAffected(tbl))
|
||||||
} else {
|
} else {
|
||||||
@@ -403,10 +434,21 @@ func GenerateSaveItemFunc(tbl schema.Table) *ast.FuncDecl {
|
|||||||
ret1 := []ast.Stmt{Comment("Do create")}
|
ret1 := []ast.Stmt{Comment("Do create")}
|
||||||
if hasCreatedAt {
|
if hasCreatedAt {
|
||||||
// Auto-timestamps: created_at
|
// Auto-timestamps: created_at
|
||||||
ret1 = append(ret1, &ast.AssignStmt{
|
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")}},
|
Lhs: []ast.Expr{&ast.SelectorExpr{X: ast.NewIdent(tbl.VarName), Sel: ast.NewIdent("CreatedAt")}},
|
||||||
Tok: token.ASSIGN,
|
Tok: token.ASSIGN,
|
||||||
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
Rhs: []ast.Expr{&ast.CallExpr{Fun: ast.NewIdent("TimestampNow"), Args: []ast.Expr{}}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return append(ret1, namedExecStmt(insertStmt)...)
|
return append(ret1, namedExecStmt(insertStmt)...)
|
||||||
@@ -477,7 +519,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
|||||||
for _, col := range cols {
|
for _, col := range cols {
|
||||||
funcParam := ast.NewIdent(col.LongGoVarName())
|
funcParam := ast.NewIdent(col.LongGoVarName())
|
||||||
funcParams.List = append(funcParams.List, &ast.Field{Names: []*ast.Ident{funcParam}, Type: GoTypeForColumn(col)})
|
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())
|
funcNameSuffix = append(funcNameSuffix, col.GoFieldName())
|
||||||
sqlParams = append(sqlParams, funcParam)
|
sqlParams = append(sqlParams, funcParam)
|
||||||
}
|
}
|
||||||
@@ -489,7 +531,7 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
|||||||
Y: SQLFieldsConstIdent(tbl),
|
Y: SQLFieldsConstIdent(tbl),
|
||||||
},
|
},
|
||||||
Op: token.ADD,
|
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{
|
return &ast.FuncDecl{
|
||||||
@@ -515,7 +557,8 @@ func GenerateGetItemBy(tbl schema.Table, cols []schema.Column) *ast.FuncDecl {
|
|||||||
&ast.IfStmt{
|
&ast.IfStmt{
|
||||||
Cond: &ast.CallExpr{
|
Cond: &ast.CallExpr{
|
||||||
Fun: &ast.SelectorExpr{X: ast.NewIdent("errors"), Sel: ast.NewIdent("Is")},
|
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")}}}},
|
Body: &ast.BlockStmt{List: []ast.Stmt{&ast.ReturnStmt{Results: []ast.Expr{&ast.CompositeLit{Type: ast.NewIdent(tbl.GoTypeName)}, ast.NewIdent("ErrNotInDB")}}}},
|
||||||
},
|
},
|
||||||
&ast.ReturnStmt{},
|
&ast.ReturnStmt{},
|
||||||
|
|||||||
61
pkg/schema/column.go
Normal file
61
pkg/schema/column.go
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
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 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())
|
||||||
|
}
|
||||||
10
pkg/schema/index.go
Normal file
10
pkg/schema/index.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
6
pkg/schema/schema.go
Normal file
6
pkg/schema/schema.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package schema
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Tables map[string]Table
|
||||||
|
Indexes map[string]Index
|
||||||
|
}
|
||||||
@@ -2,65 +2,8 @@ package schema
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.offline-twitter.com/offline-labs/gas-stack/pkg/textutils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -118,17 +61,3 @@ func (t Table) HasAutoTimestamps() (hasCreatedAt bool, hasUpdatedAt bool) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
type Schema struct {
|
|
||||||
Tables map[string]Table
|
|
||||||
Indexes map[string]Index
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user