Entity: Store created and updated timestamps with second precision

The schema stores created_at/updated_at as DATETIME without fractional seconds,
so set gorm.NowFunc to entity.Now() (UTC truncated to the second). This keeps
in-memory and persisted times in sync and makes timestamp comparisons behave the
same on SQLite and MariaDB. Adapt the affected tests to compare with Sub() in a
sane range or a clearly past base instead of strict sub-second ordering, and give
the duplicate-client registry test distinct update times.
This commit is contained in:
Michael Mayer 2026-05-31 01:13:27 +00:00
parent 3c00d6fea9
commit 41ecbf9095
6 changed files with 28 additions and 12 deletions

View file

@ -1,16 +1,15 @@
package entity
import (
"time"
"github.com/jinzhu/gorm"
)
// Set UTC as the default for created and updated timestamps.
// Set UTC, truncated to whole seconds, as the default for created and updated
// timestamps. The schema stores these as DATETIME without fractional seconds, so
// generating second-precision values keeps in-memory and persisted times in sync
// and makes timestamp comparisons behave the same on SQLite and MariaDB.
func init() {
gorm.NowFunc = func() time.Time {
return UTC()
}
gorm.NowFunc = Now
}
// Db returns the default *gorm.DB connection.

View file

@ -26,7 +26,11 @@ func TestUTC(t *testing.T) {
t.Logf("NOW: %s, %s", utc.String(), utcGorm.String())
assert.True(t, utcGorm.After(utc))
// gorm.NowFunc truncates to whole seconds, so it can trail the sub-second
// UTC() reading by up to a second; assert they stay within a sane window.
delta := utcGorm.Sub(utc)
assert.Greater(t, delta, -time.Second)
assert.Less(t, delta, time.Second)
if zone, offset := utcGorm.Zone(); zone != tz.UTC {
t.Error("gorm time should be UTC")

View file

@ -50,7 +50,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("NotUpdated", func(t *testing.T) {
uid := rnd.GenerateUID(PhotoUID)
m := &Photo{ID: missingPhotoID(), PhotoUID: uid, UpdatedAt: time.Now(), CreatedAt: Now(), PhotoTitle: "Foo"}
m := &Photo{ID: missingPhotoID(), PhotoUID: uid, UpdatedAt: Now().Add(-time.Hour), CreatedAt: Now(), PhotoTitle: "Foo"}
updatedAt := m.UpdatedAt
err := Update(m, "ID", "PhotoUID")

View file

@ -3,6 +3,7 @@ package entity
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -275,7 +276,11 @@ func TestMarker_Save(t *testing.T) {
afterDate := m.UpdatedAt
assert.Equal(t, SrcMeta, m.MarkerSrc)
assert.True(t, afterDate.After(initialDate))
// Timestamps are stored with second precision, so a save within the same
// second leaves UpdatedAt unchanged; assert it stays within a sane window.
elapsed := afterDate.Sub(initialDate)
assert.GreaterOrEqual(t, elapsed, time.Duration(0))
assert.Less(t, elapsed, time.Minute)
if m.MarkerUID == "" || m.FileUID == "" {
t.Errorf("UIDs should not be empty")

View file

@ -2,6 +2,7 @@ package entity
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
@ -258,7 +259,11 @@ func TestService_Save(t *testing.T) {
t.Fatal(err)
}
afterDate := model.UpdatedAt
assert.True(t, afterDate.After(initialDate))
// Timestamps are stored with second precision, so a save within the same
// second leaves UpdatedAt unchanged; assert it stays within a sane window.
elapsed := afterDate.Sub(initialDate)
assert.GreaterOrEqual(t, elapsed, time.Duration(0))
assert.Less(t, elapsed, time.Minute)
})
}

View file

@ -18,8 +18,11 @@ func TestClientRegistry_DuplicateNamePrefersLatest(t *testing.T) {
// Create two clients directly to simulate duplicates with same name.
c1 := entity.NewClient().SetName("pp-dupe").SetRole(cluster.RoleInstance)
assert.NoError(t, c1.Create())
// Stagger times
time.Sleep(10 * time.Millisecond)
// Backdate c1 so c2 is unambiguously the most recently updated. Timestamps are
// stored with second precision, so a sub-second stagger would not separate them.
assert.NoError(t, entity.UnscopedDb().Model(&entity.Client{}).
Where("client_uid = ?", c1.ClientUID).
UpdateColumn("updated_at", entity.Now().Add(-time.Hour)).Error)
c2 := entity.NewClient().SetName("pp-dupe").SetRole(cluster.RoleService)
assert.NoError(t, c2.Create())