refactor(time): enhance timestamp handling with utility functions
- Introduced NanoToTime and TimeToNano functions for converting between UnixNano and time.Time, improving clarity and consistency in timestamp management. - Updated Assistant model to utilize the new utility functions for created_at and updated_at fields. - Refactored Xun store methods to ensure UTC compatibility for timestamp serialization across different database drivers. - Added unit tests for the new conversion functions to validate their correctness and behavior.
This commit is contained in:
parent
8d44ef31c1
commit
1a5e1db2ec
7 changed files with 112 additions and 31 deletions
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/yaoapp/yao/agent/search"
|
||||
searchTypes "github.com/yaoapp/yao/agent/search/types"
|
||||
store "github.com/yaoapp/yao/agent/store/types"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
sui "github.com/yaoapp/yao/sui/core"
|
||||
)
|
||||
|
||||
|
|
@ -147,8 +148,8 @@ func (ast *Assistant) Map() map[string]interface{} {
|
|||
"uses": ast.Uses,
|
||||
"search": ast.Search,
|
||||
"dependencies": ast.Dependencies,
|
||||
"created_at": store.ToMySQLTime(ast.CreatedAt),
|
||||
"updated_at": store.ToMySQLTime(ast.UpdatedAt),
|
||||
"created_at": utils.NanoToTime(ast.CreatedAt),
|
||||
"updated_at": utils.NanoToTime(ast.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -326,12 +326,28 @@ func ToAssistantModel(v interface{}) (*AssistantModel, error) {
|
|||
model.CreatedAt = createdAt
|
||||
} else if createdAt, ok := data["created_at"].(float64); ok {
|
||||
model.CreatedAt = int64(createdAt)
|
||||
} else if createdAt, ok := data["created_at"].(time.Time); ok {
|
||||
model.CreatedAt = createdAt.UnixNano()
|
||||
} else if createdAt, ok := data["created_at"].(string); ok && createdAt != "" {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, createdAt); err == nil {
|
||||
model.CreatedAt = ts.UnixNano()
|
||||
} else if ts, err := time.Parse("2006-01-02 15:04:05", createdAt); err == nil {
|
||||
model.CreatedAt = ts.UnixNano()
|
||||
}
|
||||
}
|
||||
|
||||
if updatedAt, ok := data["updated_at"].(int64); ok {
|
||||
model.UpdatedAt = updatedAt
|
||||
} else if updatedAt, ok := data["updated_at"].(float64); ok {
|
||||
model.UpdatedAt = int64(updatedAt)
|
||||
} else if updatedAt, ok := data["updated_at"].(time.Time); ok {
|
||||
model.UpdatedAt = updatedAt.UnixNano()
|
||||
} else if updatedAt, ok := data["updated_at"].(string); ok && updatedAt != "" {
|
||||
if ts, err := time.Parse(time.RFC3339Nano, updatedAt); err == nil {
|
||||
model.UpdatedAt = ts.UnixNano()
|
||||
} else if ts, err := time.Parse("2006-01-02 15:04:05", updatedAt); err == nil {
|
||||
model.UpdatedAt = ts.UnixNano()
|
||||
}
|
||||
}
|
||||
|
||||
// Tags (string array)
|
||||
|
|
|
|||
|
|
@ -69,22 +69,20 @@ func (store *Xun) SaveAssistant(assistant *types.AssistantModel) (string, error)
|
|||
data["automated"] = assistant.Automated
|
||||
data["disable_global_prompts"] = assistant.DisableGlobalPrompts
|
||||
|
||||
// Set timestamps
|
||||
now := time.Now().UnixNano()
|
||||
// Use UTC time.Time so the DB driver serialises correctly for all dialects
|
||||
// (PostgreSQL timestamptz, MySQL datetime, SQLite text) with no TZ ambiguity.
|
||||
now := time.Now().UTC()
|
||||
if exists {
|
||||
// Update: set updated_at, keep created_at unchanged
|
||||
if assistant.UpdatedAt == 0 {
|
||||
data["updated_at"] = now
|
||||
} else {
|
||||
data["updated_at"] = assistant.UpdatedAt
|
||||
data["updated_at"] = nanoToTime(assistant.UpdatedAt)
|
||||
}
|
||||
// Don't modify created_at on update
|
||||
} else {
|
||||
// Create: set created_at, updated_at is null
|
||||
if assistant.CreatedAt == 0 {
|
||||
data["created_at"] = now
|
||||
} else {
|
||||
data["created_at"] = assistant.CreatedAt
|
||||
data["created_at"] = nanoToTime(assistant.CreatedAt)
|
||||
}
|
||||
data["updated_at"] = nil
|
||||
}
|
||||
|
|
@ -272,8 +270,8 @@ func (store *Xun) UpdateAssistant(assistantID string, updates map[string]interfa
|
|||
}
|
||||
}
|
||||
|
||||
// Always update updated_at timestamp
|
||||
data["updated_at"] = types.ToMySQLTime(time.Now().UnixNano())
|
||||
// Always update updated_at timestamp (UTC time.Time for dialect portability)
|
||||
data["updated_at"] = time.Now().UTC()
|
||||
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("no valid fields to update")
|
||||
|
|
|
|||
|
|
@ -65,15 +65,5 @@ func getInt64(data map[string]interface{}, key string) int64 {
|
|||
return utils.ToInt64(v)
|
||||
}
|
||||
|
||||
// toDBTime converts UnixNano timestamp to database BIGINT format
|
||||
func toDBTime(unixNano int64) int64 {
|
||||
if unixNano == 0 {
|
||||
return 0
|
||||
}
|
||||
return unixNano
|
||||
}
|
||||
|
||||
// fromDBTime converts database BIGINT timestamp to UnixNano
|
||||
func fromDBTime(dbTime int64) int64 {
|
||||
return dbTime
|
||||
}
|
||||
func nanoToTime(ns int64) time.Time { return utils.NanoToTime(ns) }
|
||||
func timeToNano(t time.Time) int64 { return utils.TimeToNano(t) }
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package xun
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/yaoapp/xun/capsule"
|
||||
|
|
@ -105,16 +106,22 @@ func TestJsonContainsValueFallback(t *testing.T) {
|
|||
assert.Equal(t, `"test"`, val, "Default (mysql) strips % wrappers")
|
||||
}
|
||||
|
||||
func TestToDBTime(t *testing.T) {
|
||||
assert.Equal(t, int64(0), toDBTime(0))
|
||||
assert.Equal(t, int64(1234567890), toDBTime(1234567890))
|
||||
assert.Equal(t, int64(-1), toDBTime(-1))
|
||||
func TestNanoToTime(t *testing.T) {
|
||||
assert.True(t, nanoToTime(0).IsZero(), "zero input returns zero time")
|
||||
|
||||
ns := int64(1609459200000000000) // 2021-01-01 00:00:00 UTC
|
||||
got := nanoToTime(ns)
|
||||
assert.Equal(t, 2021, got.Year())
|
||||
assert.Equal(t, time.January, got.Month())
|
||||
assert.Equal(t, 1, got.Day())
|
||||
assert.Equal(t, time.UTC, got.Location(), "must be UTC")
|
||||
}
|
||||
|
||||
func TestFromDBTime(t *testing.T) {
|
||||
assert.Equal(t, int64(0), fromDBTime(0))
|
||||
assert.Equal(t, int64(1234567890), fromDBTime(1234567890))
|
||||
assert.Equal(t, int64(-1), fromDBTime(-1))
|
||||
func TestTimeToNano(t *testing.T) {
|
||||
assert.Equal(t, int64(0), timeToNano(time.Time{}), "zero time returns 0")
|
||||
|
||||
ts := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
assert.Equal(t, int64(1609459200000000000), timeToNano(ts))
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
|
|||
|
|
@ -214,6 +214,24 @@ func GetTimeFormat(locale string) string {
|
|||
}
|
||||
}
|
||||
|
||||
// NanoToTime converts a UnixNano int64 to UTC time.Time.
|
||||
// Returns zero time for zero input.
|
||||
func NanoToTime(ns int64) time.Time {
|
||||
if ns == 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Unix(ns/1e9, ns%1e9).UTC()
|
||||
}
|
||||
|
||||
// TimeToNano converts time.Time to UnixNano int64.
|
||||
// Returns 0 for zero time.
|
||||
func TimeToNano(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.UnixNano()
|
||||
}
|
||||
|
||||
// DBTimeFormats contains all time formats recognized by database drivers (MySQL, PostgreSQL, SQLite).
|
||||
// Ordered from most specific to least specific for efficient parsing.
|
||||
var DBTimeFormats = []string{
|
||||
|
|
|
|||
|
|
@ -118,5 +118,56 @@ func TestToTimeStringWithPGFormats(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNanoToTime(t *testing.T) {
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
assert.True(t, NanoToTime(0).IsZero())
|
||||
})
|
||||
|
||||
t.Run("ValidTimestamp", func(t *testing.T) {
|
||||
ns := int64(1609459200000000000) // 2021-01-01 00:00:00 UTC
|
||||
got := NanoToTime(ns)
|
||||
assert.Equal(t, 2021, got.Year())
|
||||
assert.Equal(t, time.January, got.Month())
|
||||
assert.Equal(t, 1, got.Day())
|
||||
assert.Equal(t, 0, got.Hour())
|
||||
assert.Equal(t, time.UTC, got.Location())
|
||||
})
|
||||
|
||||
t.Run("PreservesNanoseconds", func(t *testing.T) {
|
||||
ns := int64(1609459200123456789)
|
||||
got := NanoToTime(ns)
|
||||
assert.Equal(t, 123456789, got.Nanosecond())
|
||||
})
|
||||
|
||||
t.Run("Negative", func(t *testing.T) {
|
||||
got := NanoToTime(-1)
|
||||
assert.False(t, got.IsZero())
|
||||
assert.Equal(t, time.UTC, got.Location())
|
||||
})
|
||||
}
|
||||
|
||||
func TestTimeToNano(t *testing.T) {
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), TimeToNano(time.Time{}))
|
||||
})
|
||||
|
||||
t.Run("ValidTime", func(t *testing.T) {
|
||||
ts := time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
assert.Equal(t, int64(1609459200000000000), TimeToNano(ts))
|
||||
})
|
||||
|
||||
t.Run("WithNanoseconds", func(t *testing.T) {
|
||||
ts := time.Date(2021, 1, 1, 0, 0, 0, 123456789, time.UTC)
|
||||
assert.Equal(t, int64(1609459200123456789), TimeToNano(ts))
|
||||
})
|
||||
|
||||
t.Run("RoundTrip", func(t *testing.T) {
|
||||
original := time.Date(2026, 3, 26, 15, 30, 45, 123456789, time.UTC)
|
||||
ns := TimeToNano(original)
|
||||
restored := NanoToTime(ns)
|
||||
assert.True(t, original.Equal(restored))
|
||||
})
|
||||
}
|
||||
|
||||
func strPtr(s string) *string { return &s }
|
||||
func int64Ptr(i int64) *int64 { return &i }
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue