Skip to content
This repository has been archived by the owner on Nov 20, 2021. It is now read-only.

Schema versioning #12

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion pkg/e2db/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import (

var db *DB

func init() {
func initDB() {
if db != nil {
return
}
log.SetLevel(zapcore.DebugLevel)

if err := os.RemoveAll("testdata"); err != nil {
Expand Down Expand Up @@ -68,6 +71,7 @@ var newRoles = []*Role{
}

func resetTable(t *testing.T) {
initDB()
roles := db.Table(&Role{})
if err := roles.Drop(); err != nil && errors.Cause(err) != ErrTableNotFound {
t.Fatal(err)
Expand Down
216 changes: 184 additions & 32 deletions pkg/e2db/model.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package e2db

import (
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"reflect"
"sort"
"strings"
"unicode"

"github.com/criticalstack/e2d/pkg/e2db/key"
"github.com/pkg/errors"
Expand All @@ -16,15 +22,50 @@ type Tag struct {
Name, Value string
}

func (t *Tag) String() string {
if t.Value == "" {
return t.Name
}
return fmt.Sprintf("%s=%s", t.Name, t.Value)
}

type FieldDef struct {
Name string
Tags []*Tag
Name string
Kind reflect.Kind
Type string
Tags []*Tag
Fields []*FieldDef
}

func (f *FieldDef) String() string {
var tags string
if len(f.Tags) > 0 {
tt := make([]string, 0)
for _, t := range f.Tags {
tt = append(tt, t.String())
}
tags = fmt.Sprintf(" `%s`", strings.Join(tt, ","))
}
t := f.Kind.String()
if f.Kind == reflect.Struct {
t = f.Type
}
return fmt.Sprintf("%s %s%s", f.Name, t, tags)
}

func (f *FieldDef) isIndex() bool {
return f.isPrimaryKey() || f.hasTag("index", "unique")
}

func (f *FieldDef) getTag(name string) *Tag {
for _, t := range f.Tags {
if t.Name == name {
return t
}
}
return nil
}

func (f *FieldDef) hasTag(tags ...string) bool {
for _, t := range f.Tags {
for _, tag := range tags {
Expand Down Expand Up @@ -55,7 +96,7 @@ const (
UniqueIndex
)

func (f *FieldDef) Type() IndexType {
func (f *FieldDef) indexType() IndexType {
switch {
case f.hasTag("increment", "id"):
return PrimaryKey
Expand All @@ -69,7 +110,7 @@ func (f *FieldDef) Type() IndexType {
}

func (f *FieldDef) indexKey(tableName string, value string) (string, error) {
switch f.Type() {
switch f.indexType() {
case PrimaryKey:
return key.ID(tableName, value), nil
case SecondaryIndex, UniqueIndex:
Expand All @@ -79,27 +120,13 @@ func (f *FieldDef) indexKey(tableName string, value string) (string, error) {
}
}

type ModelDef struct {
Name string
Fields map[string]*FieldDef

t reflect.Type
}

func NewModelDef(t reflect.Type) *ModelDef {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.NumField() == 0 {
panic("must have at least 1 struct field")
}
m := &ModelDef{
Name: t.Name(),
Fields: make(map[string]*FieldDef),
t: t,
}
func newFieldDefs(t reflect.Type) []*FieldDef {
fields := make([]*FieldDef, 0)
for i := 0; i < t.NumField(); i++ {
ft := t.Field(i)
if unicode.IsLower([]rune(ft.Name)[0]) {
continue
}
tags := make([]*Tag, 0)
if tagValue, ok := ft.Tag.Lookup("e2db"); ok {
for _, t := range strings.Split(tagValue, ",") {
Expand All @@ -111,11 +138,58 @@ func NewModelDef(t reflect.Type) *ModelDef {
}
}
}
m.Fields[ft.Name] = &FieldDef{
sort.Slice(tags, func(i, j int) bool {
return tags[i].Name < tags[j].Name
})
fd := &FieldDef{
Name: ft.Name,
Kind: ft.Type.Kind(),
Type: ft.Type.String(),
Tags: tags,
}
if ft.Type.Kind() == reflect.Struct {
fd.Fields = newFieldDefs(ft.Type)
}
fields = append(fields, fd)
}
sort.Slice(fields, func(i, j int) bool {
return fields[i].Name < fields[j].Name
})
return fields
}

type ModelDef struct {
Name string
Fields []*FieldDef
CheckSum string
Version string

t reflect.Type
}

func NewModelDef(t reflect.Type) *ModelDef {
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.NumField() == 0 {
panic("must have at least 1 struct field")
}
m := &ModelDef{
Name: t.Name(),
Fields: newFieldDefs(t),
t: t,
}
if !m.hasPrimaryKey() {
panic("must specify a primary key")
}
pk := m.getPrimaryKey()
vt := pk.getTag("v")
if vt == nil {
vt = &Tag{Name: "v", Value: "0"}
pk.Tags = append(pk.Tags, vt)
}
m.Version = vt.Value
m.CheckSum = SchemaCheckSum(m)
return m
}

Expand All @@ -127,18 +201,45 @@ func (m *ModelDef) New() *reflect.Value {
return &v
}

func (m *ModelDef) getPrimaryKey() *FieldDef {
for _, f := range m.Fields {
if f.isPrimaryKey() {
return f
}
}
return nil
}

func (m *ModelDef) hasPrimaryKey() bool {
return m.getPrimaryKey() != nil
}

func (m *ModelDef) getFieldByName(name string) (*FieldDef, bool) {
for _, f := range m.Fields {
if f.Name == name {
return f, true
}
}
return nil, false
}

func (m *ModelDef) String() string {
return m.t.String()
}

type Field struct {
*FieldDef
value reflect.Value

v reflect.Value
}

func (f *Field) isZero() bool {
return f.value.IsValid() && reflect.DeepEqual(f.value.Interface(), reflect.Zero(f.value.Type()).Interface())
return f.v.IsValid() && reflect.DeepEqual(f.v.Interface(), reflect.Zero(f.v.Type()).Interface())
}

type ModelItem struct {
*ModelDef
Fields map[string]*Field
Fields []*Field
}

func NewModelItem(v reflect.Value) *ModelItem {
Expand All @@ -148,13 +249,13 @@ func NewModelItem(v reflect.Value) *ModelItem {
}
m := &ModelItem{
ModelDef: NewModelDef(v.Type()),
Fields: make(map[string]*Field),
Fields: make([]*Field, 0),
}
for name, f := range m.ModelDef.Fields {
m.Fields[name] = &Field{
for _, f := range m.ModelDef.Fields {
m.Fields = append(m.Fields, &Field{
FieldDef: f,
value: v.FieldByName(name),
}
v: v.FieldByName(f.Name),
})
}
return m
}
Expand All @@ -167,3 +268,54 @@ func (m *ModelItem) getPrimaryKey() (*Field, error) {
}
return nil, ErrNoPrimaryKey
}

func schemaCheckSumFieldDef(f *FieldDef) string {
var sb strings.Builder
sb.WriteString(f.String())
for _, f := range f.Fields {
switch f.Kind {
case reflect.Struct:
sb.WriteString(schemaCheckSumFieldDef(f))
default:
sb.WriteString(f.String())
}
}
return sb.String()
}

func SchemaCheckSum(m *ModelDef) string {
var b bytes.Buffer
for _, f := range m.Fields {
b.WriteString(schemaCheckSumFieldDef(f))
}
h := sha1.Sum(b.Bytes())
name := hex.EncodeToString(h[:])
if len(name) > 5 {
name = name[:5]
}
return strings.ToLower(name)
}

func printFieldDef(f *FieldDef) {
fmt.Println(f)
for _, f := range f.Fields {
switch f.Kind {
case reflect.Struct:
printFieldDef(f)
default:
fmt.Println(f)
}
}
}

func PrintModelDef(m *ModelDef) {
fmt.Println(m)
for _, f := range m.Fields {
switch f.Kind {
case reflect.Struct:
printFieldDef(f)
default:
fmt.Println(f)
}
}
}
48 changes: 48 additions & 0 deletions pkg/e2db/model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package e2db_test

import (
"fmt"
"reflect"
"testing"
"time"

"github.com/criticalstack/e2d/pkg/e2db"
)

type ModelEnum int

const (
Invalid ModelEnum = iota
EnumVal1
EnumVal2
)

type NestedStruct struct {
Name string
Count int
}

type Model1 struct {
Name string `e2db:"unique,required"`
ID int `e2db:"id"`
CreatedAt time.Time
Stats NestedStruct
Enum ModelEnum
}

type Model2 struct {
ID int `e2db:"id"`
Name string `e2db:"unique,required"`
CreatedAt time.Time
Stats NestedStruct
Enum ModelEnum
}

func TestSchemaCheckSumArbitraryOrder(t *testing.T) {
m := e2db.NewModelDef(reflect.TypeOf(&Model1{}))
fmt.Println(m.String())
fmt.Println(m.CheckSum)
m = e2db.NewModelDef(reflect.TypeOf(&Model2{}))
fmt.Println(m.String())
fmt.Println(m.CheckSum)
}
Loading