diff --git a/dialects/mssql.go b/dialects/mssql.go index 0eeb1bcd..1c56e7a4 100644 --- a/dialects/mssql.go +++ b/dialects/mssql.go @@ -313,8 +313,12 @@ func (db *mssql) SQLType(c *schemas.Column) string { if c.Length == 0 { res += "(MAX)" } - case schemas.TimeStamp: - res = schemas.DateTime + case schemas.TimeStamp, schemas.DateTime: + if c.Length > 3 { + res = "DATETIME2" + } else { + return schemas.DateTime + } case schemas.TimeStampz: res = "DATETIMEOFFSET" c.Length = 7 @@ -357,7 +361,7 @@ func (db *mssql) SQLType(c *schemas.Column) string { res = t } - if res == schemas.Int || res == schemas.Bit || res == schemas.DateTime { + if res == schemas.Int || res == schemas.Bit { return res } @@ -498,6 +502,12 @@ func (db *mssql) GetColumns(queryer core.Queryer, ctx context.Context, tableName col.Length /= 2 col.Length2 /= 2 } + case "DATETIME2": + col.SQLType = schemas.SQLType{Name: schemas.DateTime, DefaultLength: 7, DefaultLength2: 0} + col.Length = scale + case "DATETIME": + col.SQLType = schemas.SQLType{Name: schemas.DateTime, DefaultLength: 3, DefaultLength2: 0} + col.Length = scale case "IMAGE": col.SQLType = schemas.SQLType{Name: schemas.VarBinary, DefaultLength: 0, DefaultLength2: 0} case "NCHAR": diff --git a/dialects/time.go b/dialects/time.go index 5aee0c10..f0bbb765 100644 --- a/dialects/time.go +++ b/dialects/time.go @@ -5,50 +5,57 @@ package dialects import ( + "strings" "time" "xorm.io/xorm/schemas" ) -// FormatTime format time as column type -func FormatTime(dialect Dialect, sqlTypeName string, t time.Time) (v interface{}) { - switch sqlTypeName { - case schemas.Time: - s := t.Format("2006-01-02 15:04:05") // time.RFC3339 - v = s[11:19] - case schemas.Date: - v = t.Format("2006-01-02") - case schemas.DateTime, schemas.TimeStamp, schemas.Varchar: // !DarthPestilane! format time when sqlTypeName is schemas.Varchar. - if dialect.URI().DBType == schemas.ORACLE { - v = t - } else { - v = t.Format("2006-01-02 15:04:05") - } - case schemas.TimeStampz: - if dialect.URI().DBType == schemas.MSSQL { - v = t.Format("2006-01-02T15:04:05.9999999Z07:00") - } else { - v = t.Format(time.RFC3339Nano) - } - case schemas.BigInt, schemas.Int: - v = t.Unix() - default: - v = t - } - return -} - // FormatColumnTime format column time -func FormatColumnTime(dialect Dialect, defaultTimeZone *time.Location, col *schemas.Column, t time.Time) (v interface{}) { +func FormatColumnTime(dialect Dialect, dbLocation *time.Location, col *schemas.Column, t time.Time) (interface{}, error) { if t.IsZero() { if col.Nullable { - return nil + return nil, nil + } + + if col.SQLType.IsNumeric() { + return 0, nil } - return "" } + var tmZone = dbLocation if col.TimeZone != nil { - return FormatTime(dialect, col.SQLType.Name, t.In(col.TimeZone)) + tmZone = col.TimeZone + } + + t = t.In(tmZone) + + switch col.SQLType.Name { + case schemas.Date: + return t.Format("2006-01-02"), nil + case schemas.Time: + var layout = "15:04:05" + if col.Length > 0 { + layout += "." + strings.Repeat("0", col.Length) + } + return t.Format(layout), nil + case schemas.DateTime, schemas.TimeStamp: + var layout = "2006-01-02 15:04:05" + if col.Length > 0 { + layout += "." + strings.Repeat("0", col.Length) + } + return t.Format(layout), nil + case schemas.Varchar: + return t.Format("2006-01-02 15:04:05"), nil + case schemas.TimeStampz: + if dialect.URI().DBType == schemas.MSSQL { + return t.Format("2006-01-02T15:04:05.9999999Z07:00"), nil + } else { + return t.Format(time.RFC3339Nano), nil + } + case schemas.BigInt, schemas.Int: + return t.Unix(), nil + default: + return t, nil } - return FormatTime(dialect, col.SQLType.Name, t.In(defaultTimeZone)) } diff --git a/engine.go b/engine.go index 20c07e13..133e9553 100644 --- a/engine.go +++ b/engine.go @@ -1226,13 +1226,13 @@ func (engine *Engine) Import(r io.Reader) ([]sql.Result, error) { } // nowTime return current time -func (engine *Engine) nowTime(col *schemas.Column) (interface{}, time.Time) { +func (engine *Engine) nowTime(col *schemas.Column) (interface{}, time.Time, error) { t := time.Now() - var tz = engine.DatabaseTZ - if !col.DisableTimeZone && col.TimeZone != nil { - tz = col.TimeZone + result, err := dialects.FormatColumnTime(engine.dialect, engine.DatabaseTZ, col, t) + if err != nil { + return nil, time.Time{}, err } - return dialects.FormatTime(engine.dialect, col.SQLType.Name, t.In(tz)), t.In(engine.TZLocation) + return result, t.In(engine.TZLocation), nil } // GetColumnMapper returns the column name mapper diff --git a/integrations/time_test.go b/integrations/time_test.go index 50fd1847..cd2e879f 100644 --- a/integrations/time_test.go +++ b/integrations/time_test.go @@ -15,8 +15,12 @@ import ( "github.com/stretchr/testify/assert" ) -func formatTime(t time.Time) string { - return t.Format("2006-01-02 15:04:05") +func formatTime(t time.Time, scales ...int) string { + var layout = "2006-01-02 15:04:05" + if len(scales) > 0 && scales[0] > 0 { + layout += "." + strings.Repeat("0", scales[0]) + } + return t.Format(layout) } func TestTimeUserTime(t *testing.T) { @@ -565,3 +569,53 @@ func TestDeletedInt64(t *testing.T) { assert.True(t, has) assert.EqualValues(t, d1, d4) } + +func TestTimestamp(t *testing.T) { + { + assert.NoError(t, PrepareEngine()) + + type TimestampStruct struct { + Id int64 + InsertTime time.Time `xorm:"DATETIME(6)"` + } + + assertSync(t, new(TimestampStruct)) + + var d1 = TimestampStruct{ + InsertTime: time.Now(), + } + cnt, err := testEngine.Insert(&d1) + assert.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + var d2 TimestampStruct + has, err := testEngine.ID(d1.Id).Get(&d2) + assert.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, formatTime(d1.InsertTime, 6), formatTime(d2.InsertTime, 6)) + } + + /*{ + assert.NoError(t, PrepareEngine()) + + type TimestampzStruct struct { + Id int64 + InsertTime time.Time `xorm:"TIMESTAMPZ"` + } + + assertSync(t, new(TimestampzStruct)) + + var d3 = TimestampzStruct{ + InsertTime: time.Now(), + } + cnt, err := testEngine.Insert(&d3) + assert.NoError(t, err) + assert.EqualValues(t, 1, cnt) + + var d4 TimestampzStruct + has, err := testEngine.ID(d3.Id).Get(&d4) + assert.NoError(t, err) + assert.True(t, has) + assert.EqualValues(t, formatTime(d3.InsertTime, 6), formatTime(d4.InsertTime, 6)) + }*/ +} diff --git a/internal/convert/time.go b/internal/convert/time.go index ecb30a3f..e53a19cd 100644 --- a/internal/convert/time.go +++ b/internal/convert/time.go @@ -8,6 +8,7 @@ import ( "database/sql" "fmt" "strconv" + "strings" "time" "xorm.io/xorm/internal/utils" @@ -39,6 +40,14 @@ func String2Time(s string, originalLocation *time.Location, convertedLocation *t } dt = dt.In(convertedLocation) return &dt, nil + } else if len(s) >= 21 && s[19] == '.' { + var layout = "2006-01-02 15:04:05." + strings.Repeat("0", len(s)-20) + dt, err := time.ParseInLocation(layout, s, originalLocation) + if err != nil { + return nil, err + } + dt = dt.In(convertedLocation) + return &dt, nil } else { i, err := strconv.ParseInt(s, 10, 64) if err == nil { diff --git a/internal/statements/statement.go b/internal/statements/statement.go index 8e3c083c..adbeb1c2 100644 --- a/internal/statements/statement.go +++ b/internal/statements/statement.go @@ -734,7 +734,11 @@ func (statement *Statement) asDBCond(fieldValue reflect.Value, fieldType reflect if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { return nil, false, nil } - return dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t), true, nil + res, err := dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t) + if err != nil { + return nil, false, err + } + return res, true, nil } else if fieldType.ConvertibleTo(schemas.BigFloatType) { t := fieldValue.Convert(schemas.BigFloatType).Interface().(big.Float) v := t.String() diff --git a/internal/statements/update.go b/internal/statements/update.go index 39a7f829..a8a174f9 100644 --- a/internal/statements/update.go +++ b/internal/statements/update.go @@ -208,7 +208,10 @@ func (statement *Statement) BuildUpdates(tableValue reflect.Value, if !requiredField && (t.IsZero() || !fieldValue.IsValid()) { continue } - val = dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t) + val, err = dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t) + if err != nil { + return nil, nil, err + } } else if nulType, ok := fieldValue.Interface().(driver.Valuer); ok { val, _ = nulType.Value() if val == nil && !requiredField { diff --git a/internal/statements/values.go b/internal/statements/values.go index ada01755..7351fb79 100644 --- a/internal/statements/values.go +++ b/internal/statements/values.go @@ -87,8 +87,8 @@ func (statement *Statement) Value2Interface(col *schemas.Column, fieldValue refl case reflect.Struct: if fieldType.ConvertibleTo(schemas.TimeType) { t := fieldValue.Convert(schemas.TimeType).Interface().(time.Time) - tf := dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t) - return tf, nil + tf, err := dialects.FormatColumnTime(statement.dialect, statement.defaultTimeZone, col, t) + return tf, err } else if fieldType.ConvertibleTo(nullFloatType) { t := fieldValue.Convert(nullFloatType).Interface().(sql.NullFloat64) if !t.Valid { diff --git a/session_delete.go b/session_delete.go index baabb558..37b9c1cd 100644 --- a/session_delete.go +++ b/session_delete.go @@ -212,7 +212,10 @@ func (session *Session) Delete(beans ...interface{}) (int64, error) { paramsLen := len(condArgs) copy(condArgs[1:paramsLen], condArgs[0:paramsLen-1]) - val, t := session.engine.nowTime(deletedColumn) + val, t, err := session.engine.nowTime(deletedColumn) + if err != nil { + return 0, err + } condArgs[0] = val var colName = deletedColumn.Name diff --git a/session_insert.go b/session_insert.go index b116b9ff..78f0e555 100644 --- a/session_insert.go +++ b/session_insert.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "xorm.io/xorm/dialects" "xorm.io/xorm/internal/convert" "xorm.io/xorm/internal/utils" "xorm.io/xorm/schemas" @@ -137,7 +138,10 @@ func (session *Session) insertMultipleStruct(rowsSlicePtr interface{}) (int64, e continue } if (col.IsCreated || col.IsUpdated) && session.statement.UseAutoTime { - val, t := session.engine.nowTime(col) + val, t, err := session.engine.nowTime(col) + if err != nil { + return 0, err + } args = append(args, val) var colName = col.Name @@ -427,29 +431,12 @@ func (session *Session) genInsertColumns(bean interface{}) ([]string, []interfac if col.MapType == schemas.ONLYFROMDB { continue } - - if col.IsDeleted { - colNames = append(colNames, col.Name) - if !col.Nullable { - if col.SQLType.IsNumeric() { - args = append(args, 0) - } else { - args = append(args, time.Time{}.Format("2006-01-02 15:04:05")) - } - } else { - args = append(args, nil) - } - continue - } - if session.statement.OmitColumnMap.Contain(col.Name) { continue } - if len(session.statement.ColumnMap) > 0 && !session.statement.ColumnMap.Contain(col.Name) { continue } - if session.statement.IncrColumns.IsColExist(col.Name) { continue } else if session.statement.DecrColumns.IsColExist(col.Name) { @@ -458,6 +445,16 @@ func (session *Session) genInsertColumns(bean interface{}) ([]string, []interfac continue } + if col.IsDeleted { + arg, err := dialects.FormatColumnTime(session.engine.dialect, session.engine.DatabaseTZ, col, time.Time{}) + if err != nil { + return nil, nil, err + } + args = append(args, arg) + colNames = append(colNames, col.Name) + continue + } + fieldValuePtr, err := col.ValueOf(bean) if err != nil { return nil, nil, err @@ -478,7 +475,10 @@ 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) + val, t, err := session.engine.nowTime(col) + if err != nil { + return nil, nil, err + } args = append(args, val) var colName = col.Name diff --git a/session_update.go b/session_update.go index 4f8e6961..7d91346e 100644 --- a/session_update.go +++ b/session_update.go @@ -215,7 +215,10 @@ func (session *Session) Update(bean interface{}, condiBean ...interface{}) (int6 !session.statement.OmitColumnMap.Contain(table.Updated) { colNames = append(colNames, session.engine.Quote(table.Updated)+" = ?") col := table.UpdatedColumn() - val, t := session.engine.nowTime(col) + val, t, err := session.engine.nowTime(col) + if err != nil { + return 0, err + } if session.engine.dialect.URI().DBType == schemas.ORACLE { args = append(args, t) } else { @@ -521,7 +524,10 @@ 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) + val, t, err := session.engine.nowTime(col) + if err != nil { + return nil, nil, err + } args = append(args, val) var colName = col.Name