diff --git a/convert/autotimer.go b/convert/autotimer.go new file mode 100644 index 00000000..5fec8a74 --- /dev/null +++ b/convert/autotimer.go @@ -0,0 +1,7 @@ +package convert + +import "time" + +type AutoTimer interface { + AutoTime(t time.Time) (interface{}, error) +} diff --git a/integrations/types_autotimer_test.go b/integrations/types_autotimer_test.go new file mode 100644 index 00000000..44ad830f --- /dev/null +++ b/integrations/types_autotimer_test.go @@ -0,0 +1,128 @@ +// Copyright 2017 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 integrations + +import ( + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type Timestamp int64 + +func FromTimeToTimestamp(t time.Time) Timestamp { + return Timestamp(int64(t.UnixNano()) / 1e6) +} + +func (t Timestamp) String() string { + return strconv.FormatInt(int64(t), 10) +} +func (t *Timestamp) Time() time.Time { + return time.Unix(int64(*t)/1e3, (int64(*t)%1e3)*1e6) +} + +func (t *Timestamp) FromDB(b []byte) error { + var err error + var value int64 + value, err = strconv.ParseInt(string(b), 10, 64) + if err != nil { + return nil + } + *t = Timestamp(value) + return nil +} + +func (t *Timestamp) ToDB() ([]byte, error) { + if t == nil { + return nil, nil + } + data := strconv.FormatInt(int64(*t), 10) + if len(data) == 0 { + return []byte("0"), nil + } + return []byte(data), nil +} + +func (t *Timestamp) AutoTime(now time.Time) (interface{}, error) { + data := int64(now.UnixNano()) / 1e6 + return data, nil +} + +type AutoTimerStruct struct { + ID string `xorm:"varchar(20) pk unique 'id'" json:"id"` + Name string `xorm:"varchar(100) notnull" json:"username"` + Description string `xorm:"text" json:"description"` + + CreatedAt Timestamp `xorm:"created bigint notnull" json:"created_at"` + UpdatedAt Timestamp `xorm:"updated bigint notnull" json:"updated_at"` + DeletedAt *Timestamp `xorm:"deleted bigint null" json:"deleted_at"` +} + +func TestAutoTimerStructInsert(t *testing.T) { + assert.NoError(t, PrepareEngine()) + assertSync(t, new(AutoTimerStruct)) + // insert + id := strconv.FormatInt(time.Now().UnixNano(), 10) + item := AutoTimerStruct{ + ID: id, + Name: "AutoTimer Test:Insert", + } + _, err := testEngine.Insert(&item) + assert.NoError(t, err) + assert.EqualValues(t, id, item.ID) + // get + var result AutoTimerStruct + has, err := testEngine.ID(id).Get(&result) + assert.NoError(t, err) + assert.True(t, has) + assert.NotEmpty(t, result.CreatedAt) + assert.NotEmpty(t, result.UpdatedAt) + assert.Nil(t, item.DeletedAt) +} +func TestAutoTimerStructUpdate(t *testing.T) { + assert.NoError(t, PrepareEngine()) + assertSync(t, new(AutoTimerStruct)) + // insert + id := strconv.FormatInt(time.Now().UnixNano(), 10) + item := AutoTimerStruct{ + ID: id, + Name: "AutoTimer Test:Update", + } + _, err := testEngine.Insert(&item) + assert.NoError(t, err) + // update + item.Description = "updated" + time.Sleep(50 * time.Millisecond) + _, err = testEngine.ID(id).Update(&item) + assert.NoError(t, err) + assert.Greater(t, item.UpdatedAt, item.CreatedAt) + assert.Nil(t, item.DeletedAt) +} + +func TestAutoTimerStructDelete(t *testing.T) { + assert.NoError(t, PrepareEngine()) + assertSync(t, new(AutoTimerStruct)) + // insert + id := strconv.FormatInt(time.Now().UnixNano(), 10) + item := AutoTimerStruct{ + ID: id, + Name: "AutoTimer Test:Delete", + } + _, err := testEngine.Insert(&item) + assert.NoError(t, err) + // delete + _, err = testEngine.ID(id).Delete(&item) + assert.NoError(t, err) + // get + var result AutoTimerStruct + has, err := testEngine.ID(id).Get(&result) + assert.NoError(t, err) + assert.True(t, has) + assert.NotEmpty(t, result.CreatedAt) + assert.NotEmpty(t, result.UpdatedAt) + assert.NotEmpty(t, result.DeletedAt) +} diff --git a/internal/statements/values.go b/internal/statements/values.go index a1102c54..c641c8c3 100644 --- a/internal/statements/values.go +++ b/internal/statements/values.go @@ -152,3 +152,44 @@ func (statement *Statement) Value2Interface(col *schemas.Column, fieldValue refl return fieldValue.Interface(), nil } } + +// IsAutoTimer a field value of a struct implements AutoTimer interface +func (statement *Statement) IsAutoTimer(fieldValue reflect.Value, t time.Time) (interface{}, bool) { + fieldType := fieldValue.Type() + if fieldValue.CanAddr() { + if fieldType.Kind() == reflect.Ptr { + // log.Println("[IsAutoTimer] is ptr") + if !fieldValue.IsZero() { + fieldValue = fieldValue.Elem() + fieldType = fieldValue.Type() + } else { + fieldValue = reflect.Zero(fieldType) + } + } + + // log.Printf("[IsAutoTimer].1 type=%v,kind=%v,addr=%v\n", fieldValue.Type(), fieldValue.Type().Kind(), fieldValue.CanAddr()) + if fieldValue.CanAddr() { + if fieldConvert, ok := fieldValue.Addr().Interface().(convert.AutoTimer); ok { + val, err := fieldConvert.AutoTime(t) + if err != nil { + return nil, false + } + // log.Println("[IsAutoTimer] Addr.OK") + return val, true + } + // log.Println("[IsAutoTimer] Addr.Fail") + return nil, false + } + // log.Printf("[IsAutoTimer].2 type=%v,addr=%v\n", fieldValue.Type(), fieldValue.CanAddr()) + if fieldConvert, ok := fieldValue.Interface().(convert.AutoTimer); ok { + val, err := fieldConvert.AutoTime(t) + if err != nil { + return nil, false + } + // log.Println("[IsAutoTimer] OK") + return val, true + } + } + // log.Println("[IsAutoTimer] Fail") + return nil, false +} diff --git a/session_cols.go b/session_cols.go index ca3589ab..d57afdf7 100644 --- a/session_cols.go +++ b/session_cols.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "xorm.io/xorm/convert" "xorm.io/xorm/schemas" ) @@ -32,6 +33,15 @@ func setColumnTime(bean interface{}, col *schemas.Column, t time.Time) { if err != nil { return } + if v.CanSet() { + if fieldConvert, ok := v.Addr().Interface().(convert.AutoTimer); ok { + _, err := fieldConvert.AutoTime(t) + if err != nil { + return + } + return + } + } if v.CanSet() { switch v.Type().Kind() { case reflect.Struct: diff --git a/session_delete.go b/session_delete.go index 13bf791f..b182cfd3 100644 --- a/session_delete.go +++ b/session_delete.go @@ -201,6 +201,14 @@ func (session *Session) Delete(bean interface{}) (int64, error) { copy(condArgs[1:paramsLen], condArgs[0:paramsLen-1]) val, t := session.engine.nowTime(deletedColumn) + fieldValuePtr, err := deletedColumn.ValueOf(bean) + if err != nil { + return 0, err + } + fieldValue := *fieldValuePtr + if at, ok := session.statement.IsAutoTimer(fieldValue, t); ok { + val = at + } condArgs[0] = val var colName = deletedColumn.Name diff --git a/session_insert.go b/session_insert.go index 5f968151..1756b561 100644 --- a/session_insert.go +++ b/session_insert.go @@ -166,6 +166,9 @@ func (session *Session) innerInsertMulti(rowsSlicePtr interface{}) (int64, error } if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { val, t := session.engine.nowTime(col) + if at, ok := session.statement.IsAutoTimer(fieldValue, t); ok { + val = at + } args = append(args, val) var colName = col.Name @@ -539,6 +542,9 @@ func (session *Session) genInsertColumns(bean interface{}) ([]string, []interfac if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { // if time is non-empty, then set to auto time val, t := session.engine.nowTime(col) + if at, ok := session.statement.IsAutoTimer(fieldValue, t); ok { + val = at + } args = append(args, val) var colName = col.Name diff --git a/session_update.go b/session_update.go index 62116c47..592eab98 100644 --- a/session_update.go +++ b/session_update.go @@ -206,6 +206,14 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?") col := table.UpdatedColumn() val, t := session.engine.nowTime(col) + fieldValuePtr, err := col.ValueOf(bean) + if err != nil { + return 0, err + } + fieldValue := *fieldValuePtr + if at, ok := session.statement.IsAutoTimer(fieldValue, t); ok { + val = at + } args = append(args, val) var colName = col.Name @@ -505,6 +513,14 @@ func (session *Session) genUpdateColumns(bean interface{}) ([]string, []interfac if col.IsUpdated && session.statement.UseAutoTime /*&& isZero(fieldValue.Interface())*/ { // if time is non-empty, then set to auto time val, t := session.engine.nowTime(col) + fieldValuePtr, err := col.ValueOf(bean) + if err != nil { + return nil, nil, err + } + fieldValue := *fieldValuePtr + if at, ok := session.statement.IsAutoTimer(fieldValue, t); ok { + val = at + } args = append(args, val) var colName = col.Name