586 lines
14 KiB
Go
586 lines
14 KiB
Go
package dbmeta
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"git.bit5.ru/backend/db"
|
|
"git.bit5.ru/backend/errors"
|
|
"git.bit5.ru/backend/meta"
|
|
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
const tracerName = "git.bit5.ru/backend/dbmeta"
|
|
|
|
var tracer = otel.Tracer(tracerName)
|
|
|
|
type SaveIncompatibleError struct {
|
|
s string
|
|
}
|
|
|
|
func (e *SaveIncompatibleError) Error() string {
|
|
return e.s
|
|
}
|
|
|
|
type dataRowInfo struct {
|
|
mType reflect.Type
|
|
data meta.IMetaStruct
|
|
pkey string
|
|
tableName string
|
|
fields []string
|
|
mapFields map[int]int
|
|
}
|
|
|
|
type RemovedIds struct {
|
|
//class_id => []ids
|
|
ids map[uint32][]uint64
|
|
}
|
|
|
|
func SaveRow(ctx context.Context, db *db.DBC, dataItem meta.IMetaStruct) error {
|
|
|
|
ctx, span := tracer.Start(ctx, "SaveRow")
|
|
defer span.End()
|
|
|
|
info, err := makeDataRowInfo(dataItem)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
fields := make([]string, 0, len(info.fields))
|
|
params := make([]interface{}, 0, len(info.fields))
|
|
elem := reflect.ValueOf(dataItem).Elem()
|
|
|
|
var pkey *reflect.Value
|
|
|
|
bitmaskItem, bitmaskable := dataItem.(meta.IBitmasked)
|
|
|
|
for fieldIdx, fieldNum := range info.mapFields {
|
|
value := elem.Field(fieldNum)
|
|
if info.fields[fieldIdx] == info.pkey && isEmptyField(value) {
|
|
pkey = &value
|
|
continue
|
|
}
|
|
|
|
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(fieldIdx))
|
|
if skipAdding {
|
|
continue
|
|
}
|
|
|
|
fields = append(fields, info.fields[fieldIdx])
|
|
params = append(params, value.Interface())
|
|
}
|
|
|
|
if len(fields) == 0 {
|
|
err := errors.New("Fields list is empty")
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
tableName := info.tableName
|
|
sqlSmt := createInsertSQLForFields(ctx, tableName, fields, 1 /*one_row*/)
|
|
|
|
span.SetAttributes(
|
|
semconv.DBSystemKey.String("mysql"),
|
|
semconv.DBSQLTableKey.String(tableName),
|
|
semconv.DBStatementKey.String(sqlSmt),
|
|
semconv.DBOperationKey.String("INSERT"),
|
|
)
|
|
|
|
updateBuilder := db.UpdateBySQL(sqlSmt, params...)
|
|
|
|
res, err := updateBuilder.ExecContext(ctx)
|
|
if err != nil {
|
|
if len(sqlSmt) > 200 {
|
|
sqlSmt = sqlSmt[0:200]
|
|
}
|
|
|
|
span.SetAttributes(
|
|
attribute.Int("sql_param_amount", len(params)),
|
|
)
|
|
|
|
// Convert SQL request parameters to slice of strings.
|
|
paramStrs := convertInterfacesToStrings(ctx, params)
|
|
if len(paramStrs) > 0 {
|
|
span.SetAttributes(
|
|
attribute.StringSlice("sql_params", paramStrs),
|
|
)
|
|
}
|
|
|
|
resultErr := errors.Errorf("Can not execute SaveRow. Got error from updateBuilder.ExecContext. %s (%s)", err.Error(), sqlSmt)
|
|
span.RecordError(resultErr)
|
|
span.SetStatus(codes.Error, resultErr.Error())
|
|
return resultErr
|
|
}
|
|
|
|
insertId, _ := res.LastInsertId()
|
|
if pkey != nil && insertId != 0 {
|
|
pkey.Set(reflect.ValueOf(uint32(insertId)).Convert(pkey.Type()))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func convertInterfacesToStrings(ctx context.Context, items []interface{}) []string {
|
|
itemAmount := len(items)
|
|
if itemAmount == 0 {
|
|
return nil
|
|
}
|
|
|
|
strs := make([]string, itemAmount)
|
|
for i, item := range items {
|
|
strs[i] = fmt.Sprint(item)
|
|
}
|
|
|
|
return strs
|
|
}
|
|
|
|
func SaveMetaRootStruct(
|
|
ctx context.Context,
|
|
db *db.DBC,
|
|
data meta.IMetaStruct,
|
|
ownerId uint32,
|
|
removedIds meta.IRemovedIds,
|
|
deltaSave bool,
|
|
) error {
|
|
|
|
spanAttrs := trace.WithAttributes(
|
|
attribute.Int64("owner.id", int64(ownerId)),
|
|
)
|
|
|
|
ctx, span := tracer.Start(ctx, "SaveMetaRootStruct", spanAttrs)
|
|
defer span.End()
|
|
|
|
dataItem := reflect.ValueOf(data)
|
|
mType := dataItem.Type()
|
|
if mType.Kind() == reflect.Ptr {
|
|
dataItem = dataItem.Elem()
|
|
mType = mType.Elem()
|
|
}
|
|
|
|
var fprops map[string]map[string]string
|
|
if dataItem.Addr().CanInterface() {
|
|
imeta, _ := dataItem.Addr().Interface().(meta.IClassProps)
|
|
if imeta != nil {
|
|
fprops = *imeta.CLASS_FIELDS_PROPS()
|
|
}
|
|
}
|
|
|
|
bitmaskItem, bitmaskable := data.(meta.IBitmasked)
|
|
|
|
numField := mType.NumField()
|
|
|
|
span.SetAttributes(
|
|
attribute.String("mType.name", mType.Name()),
|
|
attribute.Int("mType.numField", numField),
|
|
)
|
|
|
|
for i := 0; i < numField; i++ {
|
|
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(i))
|
|
if skipAdding {
|
|
continue
|
|
}
|
|
|
|
field := mType.Field(i)
|
|
tfield := field.Type
|
|
// Checking if this field should be skipped
|
|
if fprops != nil {
|
|
if props, ok := fprops[field.Name]; ok {
|
|
if _, ok := props["db_skip_save"]; ok {
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
// skip unexported fields
|
|
if len(field.PkgPath) > 0 {
|
|
continue
|
|
}
|
|
switch tfield.Kind() {
|
|
case reflect.Slice:
|
|
if err := SaveMetaCollection(ctx, db, dataItem.Field(i), ownerId, removedIds, deltaSave); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return errors.WithMessagef(err, "Can not execute SaveMetaRootStruct. Got error from SaveMetaCollection. ownerId: %d.", ownerId)
|
|
}
|
|
break
|
|
case reflect.Struct:
|
|
if err := saveStruct(ctx, db, dataItem.Field(i), ownerId); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return errors.WithMessagef(err, "Can not execute SaveMetaRootStruct. Got error from saveStruct. ownerId: %d.", ownerId)
|
|
}
|
|
break
|
|
case reflect.Map:
|
|
case reflect.Ptr:
|
|
case reflect.Array:
|
|
case reflect.UnsafePointer:
|
|
return errors.Errorf("Unsupported type: %s", tfield.Kind())
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func saveStruct(
|
|
ctx context.Context,
|
|
db *db.DBC,
|
|
dataItem reflect.Value,
|
|
ownerId uint32,
|
|
) error {
|
|
|
|
ctx, span := tracer.Start(ctx, "saveStruct")
|
|
defer span.End()
|
|
|
|
mType := dataItem.Type()
|
|
if mType.Kind() == reflect.Ptr {
|
|
dataItem = dataItem.Elem()
|
|
mType = mType.Elem()
|
|
}
|
|
|
|
var row meta.IMetaStruct
|
|
if dataItem.Addr().CanInterface() {
|
|
row, _ = dataItem.Addr().Interface().(meta.IMetaStruct)
|
|
}
|
|
if row == nil {
|
|
err := errors.Errorf("Couldn't convert to IMetaStruct: %s", mType.Name())
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
props := *row.CLASS_PROPS()
|
|
if ownerField, ok := props["owner"]; ok {
|
|
field := dataItem.FieldByName(strings.Title(ownerField))
|
|
if !field.IsValid() {
|
|
err := errors.Errorf("Owner field \"%s\" is not found in struct \"%s\"", ownerField, mType.Name())
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
// enforcing ownerId
|
|
if field.CanSet() {
|
|
field.SetUint(uint64(ownerId))
|
|
}
|
|
|
|
}
|
|
|
|
if err := SaveRow(ctx, db, row); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return errors.WithMessagef(err, "Can not execute saveStruct. Got error from SaveRow. ownerId: %d.", ownerId)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SaveMetaCollection(
|
|
ctx context.Context,
|
|
db *db.DBC,
|
|
slice reflect.Value,
|
|
ownerId uint32,
|
|
removedIds meta.IRemovedIds,
|
|
deltaSave bool,
|
|
) error {
|
|
|
|
ctx, span := tracer.Start(ctx, "SaveMetaCollection")
|
|
defer span.End()
|
|
|
|
if slice.Type().Kind() != reflect.Slice {
|
|
err := errors.Errorf("It isn't slice: %s", slice.Type().Kind())
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
sliceItem := reflect.New(slice.Type().Elem().Elem())
|
|
if sliceItem.Type().Kind() != reflect.Ptr {
|
|
sliceItem = sliceItem.Addr()
|
|
}
|
|
|
|
var row meta.IMetaStruct
|
|
if sliceItem.CanInterface() {
|
|
row, _ = sliceItem.Interface().(meta.IMetaStruct)
|
|
}
|
|
|
|
if row == nil {
|
|
err := errors.Errorf("Couldn't convert to IMetaStruct: %s", sliceItem.Kind())
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
item, _ := sliceItem.Interface().(meta.IMetaDataItem)
|
|
if item == nil {
|
|
err := errors.Errorf("Couldn't convert to IMetaDataItem: %s", slice.Type())
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
if !deltaSave {
|
|
cond := fmt.Sprintf("`%s`=%d ", item.GetOwnerFieldName(), ownerId)
|
|
_, err := db.DeleteFrom(item.GetDbTableName()).Where(cond).Exec()
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
}
|
|
|
|
collection, err := NewDataCollection(db, ownerId, "", item)
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
for ind := 0; ind < slice.Len(); ind++ {
|
|
ind_item, err := convertToIdataItem(slice.Index(ind))
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
// NOTE: we don't check here for error on purpose, here it's considered to be OK
|
|
collection.Checkin(ind_item)
|
|
}
|
|
|
|
if err := collection.Save(ctx); err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
|
|
if removedIds != nil {
|
|
classId := item.CLASS_ID()
|
|
|
|
if removedIds.HasList(classId) {
|
|
err := deleteByIds(db, ownerId, item, removedIds.GetList(classId))
|
|
if err != nil {
|
|
span.RecordError(err)
|
|
span.SetStatus(codes.Error, err.Error())
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func deleteByIds(db *db.DBC, ownerId uint32, item meta.IMetaDataItem, removedIds []uint64) error {
|
|
var cond string
|
|
|
|
if item.GetOwnerFieldName() == item.GetIdFieldName() {
|
|
cond = fmt.Sprintf("`%s`=%d ", item.GetOwnerFieldName(), ownerId)
|
|
} else {
|
|
if len(removedIds) == 0 {
|
|
return nil
|
|
}
|
|
ids := make([]string, 0, len(removedIds))
|
|
for _, id := range removedIds {
|
|
ids = append(ids, fmt.Sprintf("%d", id))
|
|
}
|
|
cond = fmt.Sprintf(
|
|
"`%s`=%d AND `%s` IN(%s)",
|
|
item.GetOwnerFieldName(),
|
|
ownerId,
|
|
item.GetIdFieldName(),
|
|
strings.Join(ids, ","))
|
|
}
|
|
|
|
_, err := db.DeleteFrom(item.GetDbTableName()).Where(cond).Exec()
|
|
return err
|
|
}
|
|
|
|
func convertToIdataItem(elem reflect.Value) (meta.IMetaDataItem, error) {
|
|
mType := elem.Type()
|
|
if mType.Kind() != reflect.Ptr {
|
|
elem = elem.Addr()
|
|
mType = elem.Type()
|
|
}
|
|
item, _ := elem.Interface().(meta.IMetaDataItem)
|
|
if item == nil {
|
|
return nil, errors.Errorf("Couldn't convert to IMetaDataItem: %s", mType.Name())
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func convertToIdataRow(elem reflect.Value) (meta.IMetaStruct, error) {
|
|
mType := elem.Type()
|
|
if mType.Kind() != reflect.Ptr {
|
|
elem = elem.Addr()
|
|
mType = elem.Type()
|
|
}
|
|
item, _ := elem.Interface().(meta.IMetaStruct)
|
|
if item == nil {
|
|
return nil, errors.Errorf("Couldn't convert to IdataRow: %s", mType.Name())
|
|
}
|
|
return item, nil
|
|
}
|
|
|
|
func makeDataRowInfo(data meta.IMetaStruct) (*dataRowInfo, error) {
|
|
info := new(dataRowInfo)
|
|
info.mType = reflect.TypeOf(data).Elem()
|
|
props := *data.CLASS_PROPS()
|
|
tableName, ok := props["table"]
|
|
info.tableName = tableName
|
|
if !ok || len(info.tableName) == 0 {
|
|
return nil, errors.Errorf("Prop 'table' is empty (%s)", info.mType.Name())
|
|
}
|
|
pkey, ok := props["pkey"]
|
|
info.pkey = ""
|
|
if ok {
|
|
info.pkey = pkey
|
|
}
|
|
info.fields = data.CLASS_FIELDS()
|
|
info.mapFields = make(map[int]int, len(info.fields))
|
|
for ind, field := range info.fields {
|
|
found := false
|
|
for i := 0; i < info.mType.NumField(); i++ {
|
|
if strings.Title(field) == info.mType.Field(i).Name {
|
|
found = true
|
|
info.mapFields[ind] = i
|
|
}
|
|
}
|
|
if !found {
|
|
return nil, errors.Errorf("Field %s[%d] not found in struct %s", field, ind, info.mType.Name())
|
|
}
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func isEmptyField(v reflect.Value) bool {
|
|
vint := reflect.ValueOf(int64(0))
|
|
vstr := reflect.ValueOf("")
|
|
if v.Type().ConvertibleTo(vint.Type()) {
|
|
return reflect.DeepEqual(v.Convert(vint.Type()).Interface(), vint.Interface())
|
|
}
|
|
return reflect.DeepEqual(v.Interface(), vstr.Interface())
|
|
}
|
|
|
|
func (rids *RemovedIds) init() {
|
|
if rids.ids == nil {
|
|
rids.ids = make(map[uint32][]uint64)
|
|
}
|
|
}
|
|
|
|
func (rids *RemovedIds) GetList(classId uint32) []uint64 {
|
|
rids.init()
|
|
if list, ok := rids.ids[classId]; ok {
|
|
return list
|
|
}
|
|
return []uint64{}
|
|
}
|
|
|
|
func (rids *RemovedIds) Add(classId uint32, id uint64) {
|
|
rids.init()
|
|
if _, ok := rids.ids[classId]; !ok {
|
|
rids.ids[classId] = []uint64{}
|
|
}
|
|
rids.ids[classId] = append(rids.ids[classId], id)
|
|
}
|
|
|
|
func (rids *RemovedIds) HasList(classId uint32) bool {
|
|
rids.init()
|
|
_, ok := rids.ids[classId]
|
|
return ok
|
|
}
|
|
|
|
func GetChangedRootStructFields(data meta.IMetaStruct, fieldNames []string) map[string]reflect.Value {
|
|
var fields = make(map[string]reflect.Value)
|
|
|
|
dataItem := reflect.ValueOf(data)
|
|
mType := dataItem.Type()
|
|
if mType.Kind() == reflect.Ptr {
|
|
dataItem = dataItem.Elem()
|
|
mType = mType.Elem()
|
|
}
|
|
|
|
bitmaskItem, bitmaskable := data.(meta.IBitmasked)
|
|
|
|
for i := 0; i < mType.NumField(); i++ {
|
|
fieldName := mType.Field(i).Name
|
|
|
|
if !sliceOfStringsContains(fieldNames, fieldName) {
|
|
continue
|
|
}
|
|
|
|
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(i))
|
|
if skipAdding {
|
|
continue
|
|
}
|
|
|
|
fields[fieldName] = dataItem.Field(i)
|
|
}
|
|
|
|
return fields
|
|
}
|
|
|
|
func sliceOfStringsContains(slice []string, v string) bool {
|
|
for _, item := range slice {
|
|
if item == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func GetChangedStructFields(dataItem reflect.Value, fieldNamesFilter func(string) bool) (map[string]reflect.Value, error) {
|
|
var fields = make(map[string]reflect.Value)
|
|
|
|
mType := dataItem.Type()
|
|
if mType.Kind() == reflect.Ptr {
|
|
dataItem = dataItem.Elem()
|
|
mType = mType.Elem()
|
|
}
|
|
|
|
var row meta.IMetaStruct
|
|
if dataItem.Addr().CanInterface() {
|
|
row, _ = dataItem.Addr().Interface().(meta.IMetaStruct)
|
|
}
|
|
if row == nil {
|
|
return nil, errors.Errorf("Couldn't convert to IMetaStruct: %s", mType.Name())
|
|
}
|
|
|
|
info, err := makeDataRowInfo(row)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
elem := reflect.ValueOf(row).Elem()
|
|
bitmaskItem, bitmaskable := row.(meta.IBitmasked)
|
|
|
|
for fieldIdx, fieldNum := range info.mapFields {
|
|
fieldName := info.fields[fieldIdx]
|
|
if fieldNamesFilter != nil && !fieldNamesFilter(fieldName) {
|
|
continue
|
|
}
|
|
|
|
value := elem.Field(fieldNum)
|
|
|
|
if fieldName == info.pkey && isEmptyField(value) {
|
|
continue
|
|
}
|
|
|
|
skipAdding := bitmaskable && bitmaskItem.IsMaskFilled() && !bitmaskItem.HasValue(uint64(fieldIdx))
|
|
if skipAdding {
|
|
continue
|
|
}
|
|
|
|
fields[fieldName] = value
|
|
}
|
|
|
|
return fields, nil
|
|
}
|