2020-03-22 15:12:55 +00:00
|
|
|
// Copyright 2020 The Xorm Authors. All rights reserved.
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package tags
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/gob"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"xorm.io/xorm/caches"
|
|
|
|
"xorm.io/xorm/convert"
|
|
|
|
"xorm.io/xorm/dialects"
|
|
|
|
"xorm.io/xorm/names"
|
|
|
|
"xorm.io/xorm/schemas"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
2021-05-15 18:32:17 +00:00
|
|
|
// ErrUnsupportedType represents an unsupported type error
|
2020-03-22 15:12:55 +00:00
|
|
|
ErrUnsupportedType = errors.New("Unsupported type")
|
|
|
|
)
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// Parser represents a parser for xorm tag
|
2020-03-22 15:12:55 +00:00
|
|
|
type Parser struct {
|
|
|
|
identifier string
|
|
|
|
dialect dialects.Dialect
|
|
|
|
columnMapper names.Mapper
|
|
|
|
tableMapper names.Mapper
|
|
|
|
handlers map[string]Handler
|
|
|
|
cacherMgr *caches.Manager
|
|
|
|
tableCache sync.Map // map[reflect.Type]*schemas.Table
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// NewParser creates a tag parser
|
2020-03-22 15:12:55 +00:00
|
|
|
func NewParser(identifier string, dialect dialects.Dialect, tableMapper, columnMapper names.Mapper, cacherMgr *caches.Manager) *Parser {
|
|
|
|
return &Parser{
|
|
|
|
identifier: identifier,
|
|
|
|
dialect: dialect,
|
|
|
|
tableMapper: tableMapper,
|
|
|
|
columnMapper: columnMapper,
|
|
|
|
handlers: defaultTagHandlers,
|
|
|
|
cacherMgr: cacherMgr,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// GetTableMapper returns table mapper
|
2020-03-22 15:12:55 +00:00
|
|
|
func (parser *Parser) GetTableMapper() names.Mapper {
|
|
|
|
return parser.tableMapper
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// SetTableMapper sets table mapper
|
2020-03-22 15:12:55 +00:00
|
|
|
func (parser *Parser) SetTableMapper(mapper names.Mapper) {
|
|
|
|
parser.ClearCaches()
|
|
|
|
parser.tableMapper = mapper
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// GetColumnMapper returns column mapper
|
2020-03-22 15:12:55 +00:00
|
|
|
func (parser *Parser) GetColumnMapper() names.Mapper {
|
|
|
|
return parser.columnMapper
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// SetColumnMapper sets column mapper
|
2020-03-22 15:12:55 +00:00
|
|
|
func (parser *Parser) SetColumnMapper(mapper names.Mapper) {
|
|
|
|
parser.ClearCaches()
|
|
|
|
parser.columnMapper = mapper
|
|
|
|
}
|
|
|
|
|
2021-05-15 18:32:17 +00:00
|
|
|
// SetIdentifier sets tag identifier
|
|
|
|
func (parser *Parser) SetIdentifier(identifier string) {
|
|
|
|
parser.ClearCaches()
|
|
|
|
parser.identifier = identifier
|
|
|
|
}
|
|
|
|
|
|
|
|
// ParseWithCache parse a struct with cache
|
2020-03-22 15:12:55 +00:00
|
|
|
func (parser *Parser) ParseWithCache(v reflect.Value) (*schemas.Table, error) {
|
|
|
|
t := v.Type()
|
|
|
|
tableI, ok := parser.tableCache.Load(t)
|
|
|
|
if ok {
|
|
|
|
return tableI.(*schemas.Table), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
table, err := parser.Parse(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
parser.tableCache.Store(t, table)
|
|
|
|
|
|
|
|
if parser.cacherMgr.GetDefaultCacher() != nil {
|
|
|
|
if v.CanAddr() {
|
|
|
|
gob.Register(v.Addr().Interface())
|
|
|
|
} else {
|
|
|
|
gob.Register(v.Interface())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return table, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClearCacheTable removes the database mapper of a type from the cache
|
|
|
|
func (parser *Parser) ClearCacheTable(t reflect.Type) {
|
|
|
|
parser.tableCache.Delete(t)
|
|
|
|
}
|
|
|
|
|
|
|
|
// ClearCaches removes all the cached table information parsed by structs
|
|
|
|
func (parser *Parser) ClearCaches() {
|
|
|
|
parser.tableCache = sync.Map{}
|
|
|
|
}
|
|
|
|
|
|
|
|
func addIndex(indexName string, table *schemas.Table, col *schemas.Column, indexType int) {
|
|
|
|
if index, ok := table.Indexes[indexName]; ok {
|
|
|
|
index.AddColumn(col.Name)
|
|
|
|
col.Indexes[index.Name] = indexType
|
|
|
|
} else {
|
|
|
|
index := schemas.NewIndex(indexName, indexType)
|
|
|
|
index.AddColumn(col.Name)
|
|
|
|
table.AddIndex(index)
|
|
|
|
col.Indexes[index.Name] = indexType
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse parses a struct as a table information
|
|
|
|
func (parser *Parser) Parse(v reflect.Value) (*schemas.Table, error) {
|
|
|
|
t := v.Type()
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
t = t.Elem()
|
2020-06-15 20:46:01 +00:00
|
|
|
v = v.Elem()
|
2020-03-22 15:12:55 +00:00
|
|
|
}
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
return nil, ErrUnsupportedType
|
|
|
|
}
|
|
|
|
|
|
|
|
table := schemas.NewEmptyTable()
|
|
|
|
table.Type = t
|
|
|
|
table.Name = names.GetTableName(parser.tableMapper, v)
|
|
|
|
|
|
|
|
var idFieldColName string
|
|
|
|
var hasCacheTag, hasNoCacheTag bool
|
|
|
|
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
tag := t.Field(i).Tag
|
|
|
|
|
|
|
|
ormTagStr := tag.Get(parser.identifier)
|
|
|
|
var col *schemas.Column
|
|
|
|
fieldValue := v.Field(i)
|
|
|
|
fieldType := fieldValue.Type()
|
|
|
|
|
|
|
|
if ormTagStr != "" {
|
|
|
|
col = &schemas.Column{
|
|
|
|
FieldName: t.Field(i).Name,
|
|
|
|
Nullable: true,
|
|
|
|
IsPrimaryKey: false,
|
|
|
|
IsAutoIncrement: false,
|
|
|
|
MapType: schemas.TWOSIDES,
|
|
|
|
Indexes: make(map[string]int),
|
|
|
|
DefaultIsEmpty: true,
|
|
|
|
}
|
|
|
|
tags := splitTag(ormTagStr)
|
|
|
|
|
|
|
|
if len(tags) > 0 {
|
|
|
|
if tags[0] == "-" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
var ctx = Context{
|
|
|
|
table: table,
|
|
|
|
col: col,
|
|
|
|
fieldValue: fieldValue,
|
|
|
|
indexNames: make(map[string]int),
|
|
|
|
parser: parser,
|
|
|
|
}
|
|
|
|
|
|
|
|
if strings.HasPrefix(strings.ToUpper(tags[0]), "EXTENDS") {
|
|
|
|
pStart := strings.Index(tags[0], "(")
|
|
|
|
if pStart > -1 && strings.HasSuffix(tags[0], ")") {
|
|
|
|
var tagPrefix = strings.TrimFunc(tags[0][pStart+1:len(tags[0])-1], func(r rune) bool {
|
|
|
|
return r == '\'' || r == '"'
|
|
|
|
})
|
|
|
|
|
|
|
|
ctx.params = []string{tagPrefix}
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := ExtendsTagHandler(&ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
for j, key := range tags {
|
|
|
|
if ctx.ignoreNext {
|
|
|
|
ctx.ignoreNext = false
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
k := strings.ToUpper(key)
|
|
|
|
ctx.tagName = k
|
|
|
|
ctx.params = []string{}
|
|
|
|
|
|
|
|
pStart := strings.Index(k, "(")
|
|
|
|
if pStart == 0 {
|
|
|
|
return nil, errors.New("( could not be the first character")
|
|
|
|
}
|
|
|
|
if pStart > -1 {
|
|
|
|
if !strings.HasSuffix(k, ")") {
|
|
|
|
return nil, fmt.Errorf("field %s tag %s cannot match ) character", col.FieldName, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx.tagName = k[:pStart]
|
|
|
|
ctx.params = strings.Split(key[pStart+1:len(k)-1], ",")
|
|
|
|
}
|
|
|
|
|
|
|
|
if j > 0 {
|
|
|
|
ctx.preTag = strings.ToUpper(tags[j-1])
|
|
|
|
}
|
|
|
|
if j < len(tags)-1 {
|
|
|
|
ctx.nextTag = tags[j+1]
|
|
|
|
} else {
|
|
|
|
ctx.nextTag = ""
|
|
|
|
}
|
|
|
|
|
|
|
|
if h, ok := parser.handlers[ctx.tagName]; ok {
|
|
|
|
if err := h(&ctx); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if strings.HasPrefix(key, "'") && strings.HasSuffix(key, "'") {
|
|
|
|
col.Name = key[1 : len(key)-1]
|
|
|
|
} else {
|
|
|
|
col.Name = key
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.hasCacheTag {
|
|
|
|
hasCacheTag = true
|
|
|
|
}
|
|
|
|
if ctx.hasNoCacheTag {
|
|
|
|
hasNoCacheTag = true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if col.SQLType.Name == "" {
|
|
|
|
col.SQLType = schemas.Type2SQLType(fieldType)
|
|
|
|
}
|
|
|
|
parser.dialect.SQLType(col)
|
|
|
|
if col.Length == 0 {
|
|
|
|
col.Length = col.SQLType.DefaultLength
|
|
|
|
}
|
|
|
|
if col.Length2 == 0 {
|
|
|
|
col.Length2 = col.SQLType.DefaultLength2
|
|
|
|
}
|
|
|
|
if col.Name == "" {
|
|
|
|
col.Name = parser.columnMapper.Obj2Table(t.Field(i).Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.isUnique {
|
|
|
|
ctx.indexNames[col.Name] = schemas.UniqueType
|
|
|
|
} else if ctx.isIndex {
|
|
|
|
ctx.indexNames[col.Name] = schemas.IndexType
|
|
|
|
}
|
|
|
|
|
|
|
|
for indexName, indexType := range ctx.indexNames {
|
|
|
|
addIndex(indexName, table, col, indexType)
|
|
|
|
}
|
|
|
|
}
|
2021-01-05 06:28:51 +00:00
|
|
|
} else if fieldValue.CanSet() {
|
2020-03-22 15:12:55 +00:00
|
|
|
var sqlType schemas.SQLType
|
|
|
|
if fieldValue.CanAddr() {
|
|
|
|
if _, ok := fieldValue.Addr().Interface().(convert.Conversion); ok {
|
|
|
|
sqlType = schemas.SQLType{Name: schemas.Text}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if _, ok := fieldValue.Interface().(convert.Conversion); ok {
|
|
|
|
sqlType = schemas.SQLType{Name: schemas.Text}
|
|
|
|
} else {
|
|
|
|
sqlType = schemas.Type2SQLType(fieldType)
|
|
|
|
}
|
|
|
|
col = schemas.NewColumn(parser.columnMapper.Obj2Table(t.Field(i).Name),
|
|
|
|
t.Field(i).Name, sqlType, sqlType.DefaultLength,
|
|
|
|
sqlType.DefaultLength2, true)
|
|
|
|
|
|
|
|
if fieldType.Kind() == reflect.Int64 && (strings.ToUpper(col.FieldName) == "ID" || strings.HasSuffix(strings.ToUpper(col.FieldName), ".ID")) {
|
|
|
|
idFieldColName = col.Name
|
|
|
|
}
|
2021-01-05 06:28:51 +00:00
|
|
|
} else {
|
|
|
|
continue
|
2020-03-22 15:12:55 +00:00
|
|
|
}
|
|
|
|
if col.IsAutoIncrement {
|
|
|
|
col.Nullable = false
|
|
|
|
}
|
|
|
|
|
|
|
|
table.AddColumn(col)
|
|
|
|
|
|
|
|
} // end for
|
|
|
|
|
|
|
|
if idFieldColName != "" && len(table.PrimaryKeys) == 0 {
|
|
|
|
col := table.GetColumn(idFieldColName)
|
|
|
|
col.IsPrimaryKey = true
|
|
|
|
col.IsAutoIncrement = true
|
|
|
|
col.Nullable = false
|
|
|
|
table.PrimaryKeys = append(table.PrimaryKeys, col.Name)
|
|
|
|
table.AutoIncrement = col.Name
|
|
|
|
}
|
|
|
|
|
|
|
|
if hasCacheTag {
|
|
|
|
if parser.cacherMgr.GetDefaultCacher() != nil { // !nash! use engine's cacher if provided
|
|
|
|
//engine.logger.Info("enable cache on table:", table.Name)
|
|
|
|
parser.cacherMgr.SetCacher(table.Name, parser.cacherMgr.GetDefaultCacher())
|
|
|
|
} else {
|
|
|
|
//engine.logger.Info("enable LRU cache on table:", table.Name)
|
|
|
|
parser.cacherMgr.SetCacher(table.Name, caches.NewLRUCacher2(caches.NewMemoryStore(), time.Hour, 10000))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if hasNoCacheTag {
|
|
|
|
//engine.logger.Info("disable cache on table:", table.Name)
|
|
|
|
parser.cacherMgr.SetCacher(table.Name, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
return table, nil
|
|
|
|
}
|