Merge branch 'dev' of github.com:vattle/sqlboiler into dev
This commit is contained in:
commit
3d22dc0897
8 changed files with 228 additions and 16 deletions
|
@ -97,6 +97,7 @@ Table of Contents
|
||||||
- Debug logging
|
- Debug logging
|
||||||
- Schemas support
|
- Schemas support
|
||||||
- 1d arrays, json, hstore & more
|
- 1d arrays, json, hstore & more
|
||||||
|
- Enum types
|
||||||
|
|
||||||
### Supported Databases
|
### Supported Databases
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
package bdb
|
package bdb
|
||||||
|
|
||||||
import "github.com/vattle/sqlboiler/strmangle"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/vattle/sqlboiler/strmangle"
|
||||||
|
)
|
||||||
|
|
||||||
// Column holds information about a database column.
|
// Column holds information about a database column.
|
||||||
// Types are Go types, converted by TranslateColumnType.
|
// Types are Go types, converted by TranslateColumnType.
|
||||||
|
@ -54,3 +58,16 @@ func FilterColumnsByDefault(defaults bool, columns []Column) []Column {
|
||||||
|
|
||||||
return cols
|
return cols
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FilterColumnsByEnum generates the list of columns that are enum values.
|
||||||
|
func FilterColumnsByEnum(columns []Column) []Column {
|
||||||
|
var cols []Column
|
||||||
|
|
||||||
|
for _, c := range columns {
|
||||||
|
if strings.HasPrefix(c.DBType, "enum") {
|
||||||
|
cols = append(cols, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cols
|
||||||
|
}
|
||||||
|
|
|
@ -66,3 +66,23 @@ func TestFilterColumnsByDefault(t *testing.T) {
|
||||||
t.Errorf("Invalid result: %#v", res)
|
t.Errorf("Invalid result: %#v", res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterColumnsByEnum(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cols := []Column{
|
||||||
|
{Name: "col1", DBType: "enum('hello')"},
|
||||||
|
{Name: "col2", DBType: "enum('hello','there')"},
|
||||||
|
{Name: "col3", DBType: "enum"},
|
||||||
|
{Name: "col4", DBType: ""},
|
||||||
|
{Name: "col5", DBType: "int"},
|
||||||
|
}
|
||||||
|
|
||||||
|
res := FilterColumnsByEnum(cols)
|
||||||
|
if res[0].Name != `col1` {
|
||||||
|
t.Errorf("Invalid result: %#v", res)
|
||||||
|
}
|
||||||
|
if res[1].Name != `col2` {
|
||||||
|
t.Errorf("Invalid result: %#v", res)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -130,7 +130,7 @@ func (p *PostgresDriver) Columns(schema, tableName string) ([]bdb.Column, error)
|
||||||
case when c.data_type = 'USER-DEFINED' and c.udt_name <> 'hstore'
|
case when c.data_type = 'USER-DEFINED' and c.udt_name <> 'hstore'
|
||||||
then
|
then
|
||||||
(
|
(
|
||||||
select 'enum(''' || string_agg(labels.label, ''',''') || ''')'
|
select 'enum.' || c.udt_name || '(''' || string_agg(labels.label, ''',''') || ''')'
|
||||||
from (
|
from (
|
||||||
select pg_enum.enumlabel as label
|
select pg_enum.enumlabel as label
|
||||||
from pg_enum
|
from pg_enum
|
||||||
|
|
|
@ -17,7 +17,9 @@ var (
|
||||||
idAlphabet = []byte("abcdefghijklmnopqrstuvwxyz")
|
idAlphabet = []byte("abcdefghijklmnopqrstuvwxyz")
|
||||||
smartQuoteRgx = regexp.MustCompile(`^(?i)"?[a-z_][_a-z0-9]*"?(\."?[_a-z][_a-z0-9]*"?)*(\.\*)?$`)
|
smartQuoteRgx = regexp.MustCompile(`^(?i)"?[a-z_][_a-z0-9]*"?(\."?[_a-z][_a-z0-9]*"?)*(\.\*)?$`)
|
||||||
|
|
||||||
rgxEnum = regexp.MustCompile(`^enum\((,?'[^']+')+\)$`)
|
rgxEnum = regexp.MustCompile(`^enum(\.[a-z_]+)?\((,?'[^']+')+\)$`)
|
||||||
|
rgxEnumIsOK = regexp.MustCompile(`^(?i)[a-z][a-z0-9_]*$`)
|
||||||
|
rgxEnumShouldTitle = regexp.MustCompile(`^[a-z][a-z0-9_]*$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var uppercaseWords = map[string]struct{}{
|
var uppercaseWords = map[string]struct{}{
|
||||||
|
@ -577,15 +579,54 @@ func GenerateIgnoreTags(tags []string) string {
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseEnum takes a string that looks like:
|
// ParseEnumVals returns the values from an enum string
|
||||||
// enum('one','two') and returns the strings one, two
|
//
|
||||||
func ParseEnum(s string) []string {
|
// Postgres and MySQL drivers return different values
|
||||||
|
// psql: enum.enum_name('values'...)
|
||||||
|
// mysql: enum('values'...)
|
||||||
|
func ParseEnumVals(s string) []string {
|
||||||
if !rgxEnum.MatchString(s) {
|
if !rgxEnum.MatchString(s) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s = strings.TrimPrefix(s, "enum('")
|
startIndex := strings.IndexByte(s, '(')
|
||||||
s = strings.TrimSuffix(s, "')")
|
s = s[startIndex+2 : len(s)-2]
|
||||||
|
|
||||||
return strings.Split(s, "','")
|
return strings.Split(s, "','")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseEnumName returns the name portion of an enum if it exists
|
||||||
|
//
|
||||||
|
// Postgres and MySQL drivers return different values
|
||||||
|
// psql: enum.enum_name('values'...)
|
||||||
|
// mysql: enum('values'...)
|
||||||
|
// In the case of mysql, the name will never return anything
|
||||||
|
func ParseEnumName(s string) string {
|
||||||
|
if !rgxEnum.MatchString(s) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
endIndex := strings.IndexByte(s, '(')
|
||||||
|
s = s[:endIndex]
|
||||||
|
startIndex := strings.IndexByte(s, '.')
|
||||||
|
if startIndex < 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return s[startIndex+1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEnumNormal checks a set of eval values to see if they're "normal"
|
||||||
|
func IsEnumNormal(values []string) bool {
|
||||||
|
for _, v := range values {
|
||||||
|
if !rgxEnumIsOK.MatchString(v) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldTitleCaseEnum checks a value to see if it's title-case-able
|
||||||
|
func ShouldTitleCaseEnum(value string) bool {
|
||||||
|
return rgxEnumShouldTitle.MatchString(value)
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package strmangle
|
package strmangle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -518,13 +517,66 @@ func TestGenerateIgnoreTags(t *testing.T) {
|
||||||
func TestParseEnum(t *testing.T) {
|
func TestParseEnum(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
vals := []string{"one", "two", "three"}
|
tests := []struct {
|
||||||
toParse := fmt.Sprintf("enum('%s')", strings.Join(vals, "','"))
|
Enum string
|
||||||
|
Name string
|
||||||
|
Vals []string
|
||||||
|
}{
|
||||||
|
{"enum('one')", "", []string{"one"}},
|
||||||
|
{"enum('one','two')", "", []string{"one", "two"}},
|
||||||
|
{"enum.working('one')", "working", []string{"one"}},
|
||||||
|
{"enum.wor_king('one','two')", "wor_king", []string{"one", "two"}},
|
||||||
|
}
|
||||||
|
|
||||||
gotVals := ParseEnum(toParse)
|
for i, test := range tests {
|
||||||
for i, v := range vals {
|
name := ParseEnumName(test.Enum)
|
||||||
if gotVals[i] != v {
|
vals := ParseEnumVals(test.Enum)
|
||||||
t.Errorf("%d) want: %s, got %s", i, v, gotVals[i])
|
if name != test.Name {
|
||||||
|
t.Errorf("%d) name was wrong, want: %s got: %s (%s)", i, test.Name, name, test.Enum)
|
||||||
|
}
|
||||||
|
for j, v := range test.Vals {
|
||||||
|
if v != vals[j] {
|
||||||
|
t.Errorf("%d.%d) value was wrong, want: %s got: %s (%s)", i, j, v, vals[j], test.Enum)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsEnumNormal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Vals []string
|
||||||
|
Ok bool
|
||||||
|
}{
|
||||||
|
{[]string{"o1ne", "two2"}, true},
|
||||||
|
{[]string{"one", "t#wo2"}, false},
|
||||||
|
{[]string{"1one", "two2"}, false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
if got := IsEnumNormal(test.Vals); got != test.Ok {
|
||||||
|
t.Errorf("%d) want: %t got: %t, %#v", i, test.Ok, got, test.Vals)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldTitleCaseEnum(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
Val string
|
||||||
|
Ok bool
|
||||||
|
}{
|
||||||
|
{"hello_there0", true},
|
||||||
|
{"hEllo", false},
|
||||||
|
{"_hello", false},
|
||||||
|
{"0hello", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
if got := ShouldTitleCaseEnum(test.Val); got != test.Ok {
|
||||||
|
t.Errorf("%d) want: %t got: %t, %v", i, test.Ok, got, test.Val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
32
templates.go
32
templates.go
|
@ -121,6 +121,28 @@ func loadTemplate(dir string, filename string) (*template.Template, error) {
|
||||||
return tpl.Lookup(filename), err
|
return tpl.Lookup(filename), err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set is to stop duplication from named enums, allowing a template loop
|
||||||
|
// to keep some state
|
||||||
|
type once map[string]struct{}
|
||||||
|
|
||||||
|
func newOnce() once {
|
||||||
|
return make(once)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o once) Has(s string) bool {
|
||||||
|
_, ok := o[s]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o once) Put(s string) bool {
|
||||||
|
if _, ok := o[s]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
o[s] = struct{}{}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// templateStringMappers are placed into the data to make it easy to use the
|
// templateStringMappers are placed into the data to make it easy to use the
|
||||||
// stringMap function.
|
// stringMap function.
|
||||||
var templateStringMappers = map[string]func(string) string{
|
var templateStringMappers = map[string]func(string) string{
|
||||||
|
@ -157,6 +179,15 @@ var templateFunctions = template.FuncMap{
|
||||||
"generateTags": strmangle.GenerateTags,
|
"generateTags": strmangle.GenerateTags,
|
||||||
"generateIgnoreTags": strmangle.GenerateIgnoreTags,
|
"generateIgnoreTags": strmangle.GenerateIgnoreTags,
|
||||||
|
|
||||||
|
// Enum ops
|
||||||
|
"parseEnumName": strmangle.ParseEnumName,
|
||||||
|
"parseEnumVals": strmangle.ParseEnumVals,
|
||||||
|
"isEnumNormal": strmangle.IsEnumNormal,
|
||||||
|
"shouldTitleCaseEnum": strmangle.ShouldTitleCaseEnum,
|
||||||
|
"onceNew": newOnce,
|
||||||
|
"oncePut": once.Put,
|
||||||
|
"onceHas": once.Has,
|
||||||
|
|
||||||
// String Map ops
|
// String Map ops
|
||||||
"makeStringMap": strmangle.MakeStringMap,
|
"makeStringMap": strmangle.MakeStringMap,
|
||||||
|
|
||||||
|
@ -173,6 +204,7 @@ var templateFunctions = template.FuncMap{
|
||||||
|
|
||||||
// dbdrivers ops
|
// dbdrivers ops
|
||||||
"filterColumnsByDefault": bdb.FilterColumnsByDefault,
|
"filterColumnsByDefault": bdb.FilterColumnsByDefault,
|
||||||
|
"filterColumnsByEnum": bdb.FilterColumnsByEnum,
|
||||||
"sqlColDefinitions": bdb.SQLColDefinitions,
|
"sqlColDefinitions": bdb.SQLColDefinitions,
|
||||||
"columnNames": bdb.ColumnNames,
|
"columnNames": bdb.ColumnNames,
|
||||||
"columnDBTypes": bdb.ColumnDBTypes,
|
"columnDBTypes": bdb.ColumnDBTypes,
|
||||||
|
|
|
@ -35,3 +35,52 @@ func makeCacheKey(wl, nzDefaults []string) string {
|
||||||
strmangle.PutBuffer(buf)
|
strmangle.PutBuffer(buf)
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
The following is a little bit of black magic and deserves some explanation
|
||||||
|
|
||||||
|
Because postgres and mysql define enums completely differently (one at the
|
||||||
|
database level as a custom datatype, and one at the table column level as
|
||||||
|
a unique thing per table)... There's a chance the enum is named (postgres)
|
||||||
|
and not (mysql). So we can't do this per table so this code is here.
|
||||||
|
|
||||||
|
We loop through each table and column looking for enums. If it's named, we
|
||||||
|
then use some disgusting magic to write state during the template compile to
|
||||||
|
the "once" map. This lets named enums only be defined once if they're referenced
|
||||||
|
multiple times in many (or even the same) tables.
|
||||||
|
|
||||||
|
Then we check if all it's values are normal, if they are we create the enum
|
||||||
|
output, if not we output a friendly error message as a comment to aid in
|
||||||
|
debugging.
|
||||||
|
|
||||||
|
Postgres output looks like: EnumNameEnumValue = "enumvalue"
|
||||||
|
MySQL output looks like: TableNameColNameEnumValue = "enumvalue"
|
||||||
|
|
||||||
|
It only titlecases the EnumValue portion if it's snake-cased.
|
||||||
|
*/}}
|
||||||
|
{{$dot := . -}}
|
||||||
|
{{$once := onceNew}}
|
||||||
|
{{- range $table := .Tables -}}
|
||||||
|
{{- range $col := $table.Columns | filterColumnsByEnum -}}
|
||||||
|
{{- $name := parseEnumName $col.DBType -}}
|
||||||
|
{{- $vals := parseEnumVals $col.DBType -}}
|
||||||
|
{{- $isNamed := ne (len $name) 0}}
|
||||||
|
{{- if and $isNamed (onceHas $once $name) -}}
|
||||||
|
{{- else -}}
|
||||||
|
{{- if $isNamed -}}
|
||||||
|
{{$_ := oncePut $once $name}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- if and (gt (len $vals) 0) (isEnumNormal $vals)}}
|
||||||
|
// Enum values for {{if $isNamed}}{{$name}}{{else}}{{$table.Name}}.{{$col.Name}}{{end}}
|
||||||
|
const (
|
||||||
|
{{- range $val := $vals -}}
|
||||||
|
{{- if $isNamed}}{{titleCase $name}}{{else}}{{titleCase $table.Name}}{{titleCase $col.Name}}{{end -}}
|
||||||
|
{{if shouldTitleCaseEnum $val}}{{titleCase $val}}{{else}}{{$val}}{{end}} = "{{$val}}"
|
||||||
|
{{end -}}
|
||||||
|
)
|
||||||
|
{{- else}}
|
||||||
|
// Enum values for {{if $isNamed}}{{$name}}{{else}}{{$table.Name}}.{{$col.Name}}{{end}} are not proper Go identifiers, cannot emit constants
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
Loading…
Reference in a new issue