From 1079e17caf327efd28c941e48decc7cde6cccaf0 Mon Sep 17 00:00:00 2001 From: Jinzhu Date: Fri, 31 Jan 2020 12:22:37 +0800 Subject: [PATCH] Implement schema parser --- model/model.go | 37 ------ schema/field.go | 202 ++++++++++++++++++++++++++++++ {model => schema}/relationship.go | 8 +- schema/schema.go | 80 ++++++++++++ schema/utils.go | 31 +++++ 5 files changed, 320 insertions(+), 38 deletions(-) delete mode 100644 model/model.go create mode 100644 schema/field.go rename {model => schema}/relationship.go (89%) create mode 100644 schema/schema.go create mode 100644 schema/utils.go diff --git a/model/model.go b/model/model.go deleted file mode 100644 index 316f3ab5..00000000 --- a/model/model.go +++ /dev/null @@ -1,37 +0,0 @@ -package model - -import ( - "reflect" -) - -type Model struct { - ModelType reflect.Type - Table string - PrioritizedPrimaryField *Field - PrimaryFields []*Field - Fields []*Field - FieldsByName map[string]*Field - FieldsByDBName map[string]*Field - Relationships Relationships -} - -type Field struct { - Name string - DBName string - DataType reflect.Type - DBDataType string - Tag reflect.StructTag - TagSettings map[string]string - PrimaryKey bool - AutoIncrement bool - Creatable bool - Updatable bool - Nullable bool - Unique bool - Precision int - Size int - HasDefaultValue bool - DefaultValue string - StructField reflect.StructField - Model *Model -} diff --git a/schema/field.go b/schema/field.go new file mode 100644 index 00000000..9d3b3033 --- /dev/null +++ b/schema/field.go @@ -0,0 +1,202 @@ +package schema + +import ( + "database/sql/driver" + "reflect" + "strconv" + "sync" + "time" +) + +type FieldType string + +const ( + Bool FieldType = "bool" + Int = "int" + Uint = "uint" + Float = "float" + String = "string" + Time = "time" + Bytes = "bytes" +) + +type Field struct { + Name string + DBName string + BindNames []string + DataType FieldType + DBDataType string + PrimaryKey bool + AutoIncrement bool + Creatable bool + Updatable bool + HasDefaultValue bool + DefaultValue string + NotNull bool + Unique bool + Comment string + Size int + Precision int + FieldType reflect.Type + StructField reflect.StructField + Tag reflect.StructTag + TagSettings map[string]string + Schema *Schema + EmbeddedbSchema *Schema + Relationship string +} + +func (schema *Schema) ParseField(fieldStruct reflect.StructField) *Field { + field := &Field{ + Name: fieldStruct.Name, + BindNames: []string{fieldStruct.Name}, + FieldType: fieldStruct.Type, + StructField: fieldStruct, + Creatable: true, + Updatable: true, + Tag: fieldStruct.Tag, + TagSettings: parseTagSetting(fieldStruct.Tag), + } + + for field.FieldType.Kind() == reflect.Ptr { + field.FieldType = field.FieldType.Elem() + } + + fieldValue := reflect.New(field.FieldType) + + // if field is valuer, used its value or first fields as data type + if valuer, isValuer := fieldValue.Interface().(driver.Valuer); isValuer { + var overrideFieldValue bool + if v, err := valuer.Value(); v != nil && err == nil { + overrideFieldValue = true + fieldValue = reflect.ValueOf(v) + } + + if field.FieldType.Kind() == reflect.Struct { + for i := 0; i < field.FieldType.NumField(); i++ { + if !overrideFieldValue { + newFieldType := field.FieldType.Field(i).Type + for newFieldType.Kind() == reflect.Ptr { + newFieldType = newFieldType.Elem() + } + + fieldValue = reflect.New(newFieldType) + overrideFieldValue = true + } + + // copy tag settings from valuer + for key, value := range parseTagSetting(field.FieldType.Field(i).Tag) { + if _, ok := field.TagSettings[key]; !ok { + field.TagSettings[key] = value + } + } + } + } + } + + // setup permission + if _, ok := field.TagSettings["-"]; ok { + field.Creatable = false + field.Updatable = false + } + + if dbName, ok := field.TagSettings["COLUMN"]; ok { + field.DBName = dbName + } + + if val, ok := field.TagSettings["PRIMARY_KEY"]; ok && checkTruth(val) { + field.PrimaryKey = true + } + + if val, ok := field.TagSettings["AUTO_INCREMENT"]; ok && checkTruth(val) { + field.AutoIncrement = true + field.HasDefaultValue = true + } + + if v, ok := field.TagSettings["DEFAULT"]; ok { + field.HasDefaultValue = true + field.DefaultValue = v + } + + if num, ok := field.TagSettings["SIZE"]; ok { + field.Size, _ = strconv.Atoi(num) + } + + if p, ok := field.TagSettings["PRECISION"]; ok { + field.Precision, _ = strconv.Atoi(p) + } + + if val, ok := field.TagSettings["NOT NULL"]; ok && checkTruth(val) { + field.NotNull = true + } + + if val, ok := field.TagSettings["UNIQUE"]; ok && checkTruth(val) { + field.Unique = true + } + + if val, ok := field.TagSettings["COMMENT"]; ok { + field.Comment = val + } + + if val, ok := field.TagSettings["TYPE"]; ok { + field.DBDataType = val + } + + switch fieldValue.Kind() { + case reflect.Bool: + field.DataType = Bool + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + field.DataType = Int + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + field.DataType = Uint + case reflect.Float32, reflect.Float64: + field.DataType = Float + case reflect.String: + field.DataType = String + case reflect.Struct: + if _, ok := fieldValue.Interface().(time.Time); ok { + field.DataType = Time + } + case reflect.Array, reflect.Slice: + if fieldValue.Type().Elem() == reflect.TypeOf(uint8(0)) { + field.DataType = Bytes + } + } + + if field.Size == 0 { + switch fieldValue.Kind() { + case reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Float64: + field.Size = 64 + case reflect.Int8, reflect.Uint8: + field.Size = 8 + case reflect.Int16, reflect.Uint16: + field.Size = 16 + case reflect.Int32, reflect.Uint32, reflect.Float32: + field.Size = 32 + } + } + + if _, ok := field.TagSettings["EMBEDDED"]; ok || fieldStruct.Anonymous { + field.EmbeddedbSchema = Parse(fieldValue, sync.Map{}) + for _, ef := range field.EmbeddedbSchema.Fields { + ef.BindNames = append([]string{fieldStruct.Name}, ef.BindNames...) + + if prefix, ok := field.TagSettings["EMBEDDED_PREFIX"]; ok { + ef.DBName = prefix + ef.DBName + } + + for k, v := range field.TagSettings { + ef.TagSettings[k] = v + } + } + } else { + switch fieldValue.Kind() { + case reflect.Struct: + field.Relationship = "one" + case reflect.Slice: + field.Relationship = "many" + } + } + + return field +} diff --git a/model/relationship.go b/schema/relationship.go similarity index 89% rename from model/relationship.go rename to schema/relationship.go index 60b0751e..b0c630be 100644 --- a/model/relationship.go +++ b/schema/relationship.go @@ -1,4 +1,4 @@ -package model +package schema // RelationshipType relationship type type RelationshipType string @@ -35,3 +35,9 @@ type JoinTable struct { ForeignKeys []*RelationField AssociationForeignKeys []*RelationField } + +func (schema *Schema) buildToOneRel(field *Field) { +} + +func (schema *Schema) buildToManyRel(field *Field) { +} diff --git a/schema/schema.go b/schema/schema.go new file mode 100644 index 00000000..6d85af8c --- /dev/null +++ b/schema/schema.go @@ -0,0 +1,80 @@ +package schema + +import ( + "go/ast" + "reflect" + "strings" + "sync" +) + +type Schema struct { + ModelType reflect.Type + Table string + PrioritizedPrimaryField *Field + PrimaryFields []*Field + Fields []*Field + FieldsByName map[string]*Field + FieldsByDBName map[string]*Field + Relationships Relationships +} + +// get data type from dialector +func Parse(dest interface{}, cacheStore sync.Map) *Schema { + modelType := reflect.ValueOf(dest).Type() + for modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Ptr { + modelType = modelType.Elem() + } + + if modelType.Kind() != reflect.Struct { + return nil + } + + if v, ok := cacheStore.Load(modelType); ok { + return v.(*Schema) + } + + schema := &Schema{ + ModelType: modelType, + FieldsByName: map[string]*Field{}, + FieldsByDBName: map[string]*Field{}, + } + + for i := 0; i < modelType.NumField(); i++ { + fieldStruct := modelType.Field(i) + if !ast.IsExported(fieldStruct.Name) { + continue + } + + schema.Fields = append(schema.Fields, schema.ParseField(fieldStruct)) + // db namer + } + + for _, field := range schema.Fields { + if field.DBName != "" { + // nonexistence or shortest path or first appear prioritized + if v, ok := schema.FieldsByDBName[field.DBName]; !ok || len(field.BindNames) < len(v.BindNames) { + schema.FieldsByDBName[field.DBName] = field + schema.FieldsByName[field.Name] = field + } + } + + if _, ok := schema.FieldsByName[field.Name]; !ok { + schema.FieldsByName[field.Name] = field + } + } + + for db, field := range schema.FieldsByDBName { + if strings.ToLower(db) == "id" { + schema.PrioritizedPrimaryField = field + } + + if field.PrimaryKey { + if schema.PrioritizedPrimaryField == nil { + schema.PrioritizedPrimaryField = field + } + schema.PrimaryFields = append(schema.PrimaryFields, field) + } + } + + return schema +} diff --git a/schema/utils.go b/schema/utils.go new file mode 100644 index 00000000..1b0f5eac --- /dev/null +++ b/schema/utils.go @@ -0,0 +1,31 @@ +package schema + +import ( + "reflect" + "strings" +) + +func parseTagSetting(tags reflect.StructTag) map[string]string { + setting := map[string]string{} + + for _, value := range strings.Split(tags.Get("gorm"), ";") { + if value != "" { + v := strings.Split(value, ":") + k := strings.TrimSpace(strings.ToUpper(v[0])) + + if len(v) >= 2 { + setting[k] = strings.Join(v[1:], ":") + } else { + setting[k] = k + } + } + } + return setting +} + +func checkTruth(val string) bool { + if strings.ToLower(val) == "false" { + return false + } + return true +}