schema/field: json type support (#38)

Summary:
Pull Request resolved: https://github.com/facebookincubator/ent/pull/38

Only `IsNil` and `NotNil` predicates are supported this moment

Reviewed By: alexsn

Differential Revision: D17444976

fbshipit-source-id: 37336fa0bc7749af995933baee2e23bb7366dd78
This commit is contained in:
Ariel Mashraki
2019-09-19 04:58:21 -07:00
committed by Facebook Github Bot
parent 83d0063437
commit c3955a08f1
214 changed files with 4005 additions and 1296 deletions

View File

@@ -7,116 +7,17 @@ package field
import (
"errors"
"math"
"reflect"
"regexp"
"strconv"
"strings"
"time"
)
// Type is a field type.
type Type uint
// Field types.
const (
TypeInvalid Type = iota
TypeBool
TypeTime
TypeBytes
TypeString
TypeInt8
TypeInt16
TypeInt32
TypeInt
TypeInt64
TypeUint8
TypeUint16
TypeUint32
TypeUint
TypeUint64
TypeFloat32
TypeFloat64
endTypes
)
func (t Type) String() string {
if int(t) < len(typeNames) {
return typeNames[t]
}
return "type" + strconv.Itoa(int(t))
}
// Valid reports if the given type if known type.
func (t Type) Valid() bool { return t > TypeInvalid && t < endTypes }
// Numeric reports if the given type is a numeric type.
func (t Type) Numeric() bool { return t >= TypeInt && t < endTypes }
// Slice reports if the given type is a slice type.
func (t Type) Slice() bool { return t == TypeBytes }
// ConstName returns the constant name of a type. It's used by entc for printing the constant name in templates.
func (t Type) ConstName() string {
switch t {
case TypeTime:
return "TypeTime"
case TypeBytes:
return "TypeBytes"
default:
return "Type" + strings.Title(t.String())
}
}
// Bits returns the size of the type in bits.
// It panics if the type is not numeric type.
func (t Type) Bits() int {
if !t.Numeric() {
panic("schema/field: Bits of non-numeric type")
}
return bits[t]
}
var (
typeNames = [...]string{
TypeInvalid: "invalid",
TypeBool: "bool",
TypeTime: "time.Time",
TypeBytes: "[]byte",
TypeString: "string",
TypeInt: "int",
TypeInt8: "int8",
TypeInt16: "int16",
TypeInt32: "int32",
TypeInt64: "int64",
TypeUint: "uint",
TypeUint8: "uint8",
TypeUint16: "uint16",
TypeUint32: "uint32",
TypeUint64: "uint64",
TypeFloat32: "float32",
TypeFloat64: "float64",
}
bits = [...]int{
TypeInt: strconv.IntSize,
TypeInt8: 8,
TypeInt16: 16,
TypeInt32: 32,
TypeInt64: 64,
TypeUint: strconv.IntSize,
TypeUint8: 8,
TypeUint16: 16,
TypeUint32: 32,
TypeUint64: 64,
TypeFloat32: 32,
TypeFloat64: 64,
}
)
// A Descriptor for field configuration.
type Descriptor struct {
Tag string // struct tag.
Size int // varchar size.
Name string // field name.
Type Type // field type.
Info *TypeInfo // field type info.
Unique bool // unique index of field.
Nillable bool // nillable struct field.
Optional bool // nullable field in database.
@@ -129,29 +30,87 @@ type Descriptor struct {
// String returns a new Field with type string.
func String(name string) *stringBuilder {
return &stringBuilder{desc: &Descriptor{Type: TypeString, Name: name}}
return &stringBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeString},
}}
}
// Text returns a new string field without limitation on the size.
// In MySQL, it is the "longtext" type, but in SQLite and Gremlin it has not effect.
func Text(name string) *stringBuilder {
return &stringBuilder{desc: &Descriptor{Type: TypeString, Name: name, Size: math.MaxInt32}}
return &stringBuilder{&Descriptor{
Name: name,
Size: math.MaxInt32,
Info: &TypeInfo{Type: TypeString},
}}
}
// Bytes returns a new Field with type bytes/buffer.
// In MySQL and SQLite, it is the "BLOB" type, and it does not support for Gremlin.
func Bytes(name string) *bytesBuilder {
return &bytesBuilder{desc: &Descriptor{Type: TypeBytes, Name: name}}
return &bytesBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeBytes, Nillable: true},
}}
}
// Bool returns a new Field with type bool.
func Bool(name string) *boolBuilder {
return &boolBuilder{desc: &Descriptor{Type: TypeBool, Name: name}}
return &boolBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeBool},
}}
}
// Time returns a new Field with type timestamp.
func Time(name string) *timeBuilder {
return &timeBuilder{desc: &Descriptor{Type: TypeTime, Name: name}}
return &timeBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeTime, PkgPath: "time"},
}}
}
// JSON returns a new Field with type json that is serialized to the given object.
// For example:
//
// field.JSON("dirs", []http.Dir{}).
// Optional()
//
//
// field.JSON("info", &Info{}).
// Optional()
//
func JSON(name string, typ interface{}) *jsonsBuilder {
t := reflect.TypeOf(typ)
info := &TypeInfo{
Type: TypeJSON,
Ident: t.String(),
PkgPath: t.PkgPath(),
}
switch t.Kind() {
case reflect.Slice, reflect.Array, reflect.Ptr, reflect.Map:
info.Nillable = true
}
return &jsonsBuilder{&Descriptor{
Name: name,
Info: info,
}}
}
// Strings returns a new JSON Field with type []string.
func Strings(name string) *jsonsBuilder {
return JSON(name, []string{})
}
// Ints returns a new JSON Field with type []int.
func Ints(name string) *jsonsBuilder {
return JSON(name, []int{})
}
// Floats returns a new JSON Field with type []float.
func Floats(name string) *jsonsBuilder {
return JSON(name, []float64{})
}
// stringBuilder is the builder for string fields.
@@ -442,3 +401,38 @@ func (b *bytesBuilder) StorageKey(key string) *bytesBuilder {
func (b *bytesBuilder) Descriptor() *Descriptor {
return b.desc
}
// jsonsBuilder is the builder for json fields.
type jsonsBuilder struct {
desc *Descriptor
}
// StorageKey sets the storage key of the field.
// In SQL dialects is the column name and Gremlin is the property.
func (b *jsonsBuilder) StorageKey(key string) *jsonsBuilder {
b.desc.StorageKey = key
return b
}
// Optional indicates that this field is optional on create.
// Unlike edges, fields are required by default.
func (b *jsonsBuilder) Optional() *jsonsBuilder {
b.desc.Optional = true
return b
}
// Immutable indicates that this field cannot be updated.
func (b *jsonsBuilder) Immutable() *jsonsBuilder {
b.desc.Immutable = true
return b
}
// Comment sets the comment of the field.
func (b *jsonsBuilder) Comment(c string) *jsonsBuilder {
return b
}
// Descriptor implements the ent.Field interface by returning its descriptor.
func (b *jsonsBuilder) Descriptor() *Descriptor {
return b.desc
}

View File

@@ -5,10 +5,13 @@
package field_test
import (
"net/http"
"regexp"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/facebookincubator/ent/schema/field"
"github.com/stretchr/testify/assert"
@@ -18,7 +21,7 @@ func TestInt(t *testing.T) {
f := field.Int("age").Positive()
fd := f.Descriptor()
assert.Equal(t, "age", fd.Name)
assert.Equal(t, field.TypeInt, fd.Type)
assert.Equal(t, field.TypeInt, fd.Info.Type)
assert.Len(t, fd.Validators, 1)
f = field.Int("age").Default(10).Min(10).Max(20)
@@ -34,17 +37,17 @@ func TestInt(t *testing.T) {
assert.False(t, fd.Immutable)
assert.Len(t, fd.Validators, 1)
assert.Equal(t, field.TypeInt8, field.Int8("age").Descriptor().Type)
assert.Equal(t, field.TypeInt16, field.Int16("age").Descriptor().Type)
assert.Equal(t, field.TypeInt32, field.Int32("age").Descriptor().Type)
assert.Equal(t, field.TypeInt64, field.Int64("age").Descriptor().Type)
assert.Equal(t, field.TypeInt8, field.Int8("age").Descriptor().Info.Type)
assert.Equal(t, field.TypeInt16, field.Int16("age").Descriptor().Info.Type)
assert.Equal(t, field.TypeInt32, field.Int32("age").Descriptor().Info.Type)
assert.Equal(t, field.TypeInt64, field.Int64("age").Descriptor().Info.Type)
}
func TestFloat(t *testing.T) {
f := field.Float("age").Positive()
fd := f.Descriptor()
assert.Equal(t, "age", fd.Name)
assert.Equal(t, field.TypeFloat64, fd.Type)
assert.Equal(t, field.TypeFloat64, fd.Info.Type)
assert.Len(t, fd.Validators, 1)
f = field.Float("age").Min(2.5).Max(5)
@@ -56,7 +59,7 @@ func TestBool(t *testing.T) {
f := field.Bool("active").Default(true).Immutable()
fd := f.Descriptor()
assert.Equal(t, "active", fd.Name)
assert.Equal(t, field.TypeBool, fd.Type)
assert.Equal(t, field.TypeBool, fd.Info.Type)
assert.NotNil(t, fd.Default)
assert.True(t, fd.Immutable)
assert.Equal(t, true, fd.Default)
@@ -66,7 +69,7 @@ func TestBytes(t *testing.T) {
f := field.Bytes("active").Default([]byte("{}"))
fd := f.Descriptor()
assert.Equal(t, "active", fd.Name)
assert.Equal(t, field.TypeBytes, fd.Type)
assert.Equal(t, field.TypeBytes, fd.Info.Type)
assert.NotNil(t, fd.Default)
assert.Equal(t, []byte("{}"), fd.Default)
}
@@ -75,7 +78,7 @@ func TestString(t *testing.T) {
re := regexp.MustCompile("[a-zA-Z0-9]")
f := field.String("name").Unique().Match(re).Validate(func(string) error { return nil })
fd := f.Descriptor()
assert.Equal(t, field.TypeString, fd.Type)
assert.Equal(t, field.TypeString, fd.Info.Type)
assert.Equal(t, "name", fd.Name)
assert.True(t, fd.Unique)
assert.Len(t, fd.Validators, 2)
@@ -89,8 +92,8 @@ func TestTime(t *testing.T) {
}).
Descriptor()
assert.Equal(t, "created_at", fd.Name)
assert.Equal(t, field.TypeTime, fd.Type)
assert.Equal(t, "time.Time", fd.Type.String())
assert.Equal(t, field.TypeTime, fd.Info.Type)
assert.Equal(t, "time.Time", fd.Info.Type.String())
assert.NotNil(t, fd.Default)
assert.Equal(t, now, fd.Default.(func() time.Time)())
@@ -103,6 +106,35 @@ func TestTime(t *testing.T) {
assert.Equal(t, now, fd.UpdateDefault.(func() time.Time)())
}
func TestJSON(t *testing.T) {
fd := field.JSON("name", map[string]string{}).
Optional().
Descriptor()
require.True(t, fd.Optional)
require.Empty(t, fd.Info.PkgPath)
require.Equal(t, "name", fd.Name)
require.Equal(t, field.TypeJSON, fd.Info.Type)
require.Equal(t, "map[string]string", fd.Info.String())
fd = field.JSON("dir", http.Dir("dir")).
Optional().
Descriptor()
require.True(t, fd.Optional)
require.Equal(t, field.TypeJSON, fd.Info.Type)
require.Equal(t, "dir", fd.Name)
require.Equal(t, "net/http", fd.Info.PkgPath)
require.Equal(t, "http.Dir", fd.Info.String())
fd = field.Strings("strings").
Optional().
Descriptor()
require.True(t, fd.Optional)
require.Empty(t, fd.Info.PkgPath)
require.Equal(t, "strings", fd.Name)
require.Equal(t, field.TypeJSON, fd.Info.Type)
require.Equal(t, "[]string", fd.Info.String())
}
func TestField_Tag(t *testing.T) {
fd := field.Bool("expired").
StructTag(`json:"expired,omitempty"`).

View File

@@ -12,14 +12,26 @@ import "errors"
{{ range $_, $t := $.Ints }}
{{ $title := title $t.String }}
// {{ $title }} returns a new Field with type {{ $t }}.
func {{ $title }}(name string) *{{ $t }}Builder { return &{{ $t }}Builder{desc: &Descriptor{Type: Type{{ $title }}, Name: name}} }
func {{ $title }}(name string) *{{ $t }}Builder { return &{{ $t }}Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: Type{{ $title }} },
}}
}
{{ end }}
// Float returns a new Field with type float64.
func Float(name string) *float64Builder { return &float64Builder{desc: &Descriptor{Type: TypeFloat64, Name: name}} }
func Float(name string) *float64Builder { return &float64Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeFloat64},
}}
}
// Float32 returns a new Field with type float32.
func Float32(name string) *float32Builder { return &float32Builder{desc: &Descriptor{Type: TypeFloat32, Name: name}} }
func Float32(name string) *float32Builder { return &float32Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeFloat32},
}}
}
{{ range $_, $t := $.Ints }}
{{ $builder := printf "%sBuilder" $t }}

View File

@@ -9,61 +9,99 @@ import "errors"
//go:generate go run gen/gen.go
// Int returns a new Field with type int.
func Int(name string) *intBuilder { return &intBuilder{desc: &Descriptor{Type: TypeInt, Name: name}} }
func Int(name string) *intBuilder {
return &intBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeInt},
}}
}
// Uint returns a new Field with type uint.
func Uint(name string) *uintBuilder {
return &uintBuilder{desc: &Descriptor{Type: TypeUint, Name: name}}
return &uintBuilder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeUint},
}}
}
// Int8 returns a new Field with type int8.
func Int8(name string) *int8Builder {
return &int8Builder{desc: &Descriptor{Type: TypeInt8, Name: name}}
return &int8Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeInt8},
}}
}
// Int16 returns a new Field with type int16.
func Int16(name string) *int16Builder {
return &int16Builder{desc: &Descriptor{Type: TypeInt16, Name: name}}
return &int16Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeInt16},
}}
}
// Int32 returns a new Field with type int32.
func Int32(name string) *int32Builder {
return &int32Builder{desc: &Descriptor{Type: TypeInt32, Name: name}}
return &int32Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeInt32},
}}
}
// Int64 returns a new Field with type int64.
func Int64(name string) *int64Builder {
return &int64Builder{desc: &Descriptor{Type: TypeInt64, Name: name}}
return &int64Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeInt64},
}}
}
// Uint8 returns a new Field with type uint8.
func Uint8(name string) *uint8Builder {
return &uint8Builder{desc: &Descriptor{Type: TypeUint8, Name: name}}
return &uint8Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeUint8},
}}
}
// Uint16 returns a new Field with type uint16.
func Uint16(name string) *uint16Builder {
return &uint16Builder{desc: &Descriptor{Type: TypeUint16, Name: name}}
return &uint16Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeUint16},
}}
}
// Uint32 returns a new Field with type uint32.
func Uint32(name string) *uint32Builder {
return &uint32Builder{desc: &Descriptor{Type: TypeUint32, Name: name}}
return &uint32Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeUint32},
}}
}
// Uint64 returns a new Field with type uint64.
func Uint64(name string) *uint64Builder {
return &uint64Builder{desc: &Descriptor{Type: TypeUint64, Name: name}}
return &uint64Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeUint64},
}}
}
// Float returns a new Field with type float64.
func Float(name string) *float64Builder {
return &float64Builder{desc: &Descriptor{Type: TypeFloat64, Name: name}}
return &float64Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeFloat64},
}}
}
// Float32 returns a new Field with type float32.
func Float32(name string) *float32Builder {
return &float32Builder{desc: &Descriptor{Type: TypeFloat32, Name: name}}
return &float32Builder{&Descriptor{
Name: name,
Info: &TypeInfo{Type: TypeFloat32},
}}
}
// intBuilder is the builder for int field.

105
schema/field/type.go Normal file
View File

@@ -0,0 +1,105 @@
package field
import "strings"
type Type uint8
const (
TypeInvalid Type = iota
TypeBool
TypeTime
TypeJSON
TypeBytes
TypeString
TypeInt8
TypeInt16
TypeInt32
TypeInt
TypeInt64
TypeUint8
TypeUint16
TypeUint32
TypeUint
TypeUint64
TypeFloat32
TypeFloat64
endTypes
)
// String returns the string representation of a type.
func (t Type) String() string {
if t < endTypes {
return typeNames[t]
}
return typeNames[TypeInvalid]
}
// Numeric reports if the given type is a numeric type.
func (t Type) Numeric() bool {
return t >= TypeInt8 && t < endTypes
}
// ConstName returns the constant name of a info type.
// It's used by entc for printing the constant name in templates.
func (t Type) ConstName() string {
switch t {
case TypeJSON:
return "TypeJSON"
case TypeTime:
return "TypeTime"
case TypeBytes:
return "TypeBytes"
default:
return "Type" + strings.Title(typeNames[t])
}
}
type TypeInfo struct {
Type Type
Ident string
PkgPath string
Nillable bool // slices or pointers.
}
// String returns the string representation of a type.
func (t TypeInfo) String() string {
switch {
case t.Ident != "":
return t.Ident
case t.Type < endTypes:
return typeNames[t.Type]
default:
return typeNames[TypeInvalid]
}
}
// Valid reports if the given type if known type.
func (t TypeInfo) Valid() bool {
return t.Type > TypeInvalid && t.Type < endTypes
}
// Numeric reports if the given type is a numeric type.
func (t TypeInfo) Numeric() bool {
return t.Type.Numeric()
}
var typeNames = [...]string{
TypeInvalid: "invalid",
TypeBool: "bool",
TypeTime: "time.Time",
TypeJSON: "json.RawMessage",
TypeBytes: "[]byte",
TypeString: "string",
TypeInt: "int",
TypeInt8: "int8",
TypeInt16: "int16",
TypeInt32: "int32",
TypeInt64: "int64",
TypeUint: "uint",
TypeUint8: "uint8",
TypeUint16: "uint16",
TypeUint32: "uint32",
TypeUint64: "uint64",
TypeFloat32: "float32",
TypeFloat64: "float64",
}