Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(collections): add a time.Time key codec #19879

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions collections/codec/correctness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package codec_test

import (
"testing"
"time"

"cosmossdk.io/collections"
"cosmossdk.io/collections/colltest"
Expand Down Expand Up @@ -48,4 +49,7 @@ func TestKeyCorrectness(t *testing.T) {
collections.Join("hello", "testing"),
)
})
t.Run("time.Time", func(t *testing.T) {
colltest.TestKeyCodec(t, collections.TimeKey, time.UnixMilli(time.Now().UnixMilli()).UTC())
})
Comment on lines +52 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using time.Now() in tests can introduce non-determinism, which might lead to flaky tests. Consider using a fixed time.Time value to ensure the test's predictability and reliability. For example:

- colltest.TestKeyCodec(t, collections.TimeKey, time.UnixMilli(time.Now().UnixMilli()).UTC())
+ colltest.TestKeyCodec(t, collections.TimeKey, time.Date(2024, 3, 29, 0, 0, 0, 0, time.UTC))

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
t.Run("time.Time", func(t *testing.T) {
colltest.TestKeyCodec(t, collections.TimeKey, time.UnixMilli(time.Now().UnixMilli()).UTC())
})
t.Run("time.Time", func(t *testing.T) {
colltest.TestKeyCodec(t, collections.TimeKey, time.Date(2024, 3, 29, 0, 0, 0, 0, time.UTC))
})

}
54 changes: 54 additions & 0 deletions collections/codec/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package codec

import (
"encoding/binary"
"fmt"
"time"
)

chixiaowen marked this conversation as resolved.
Show resolved Hide resolved
var timeSize = 8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idk if 8 bytes gives you the precision you're looking for, assuming the time is represented as Join(seconds,milliseconds)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using a 64-bit integer to directly represent a Unix timestamp with millisecond precision usually provides sufficient precision and range, eliminating the need to separately represent seconds and milliseconds.


type timeKey struct{}

func NewTimeKey() KeyCodec[time.Time] { return timeKey{} }

func (t timeKey) Encode(buffer []byte, key time.Time) (int, error) {
if len(buffer) < timeSize {
return 0, fmt.Errorf("buffer too small, required at least 8 bytes")
}
millis := key.UTC().UnixNano() / int64(time.Millisecond)
binary.BigEndian.PutUint64(buffer, uint64(millis))
return timeSize, nil
}

func (t timeKey) Decode(buffer []byte) (int, time.Time, error) {
if len(buffer) != timeSize {
return 0, time.Time{}, fmt.Errorf("invalid time buffer buffer size")
}
millis := int64(binary.BigEndian.Uint64(buffer))
return timeSize, time.UnixMilli(millis).UTC(), nil
}

func (t timeKey) Size(_ time.Time) int { return timeSize }

func (t timeKey) EncodeJSON(value time.Time) ([]byte, error) { return value.MarshalJSON() }

func (t timeKey) DecodeJSON(b []byte) (time.Time, error) {
time := time.Time{}
err := time.UnmarshalJSON(b)
return time, err
}

func (t timeKey) Stringify(key time.Time) string { return key.String() }
func (t timeKey) KeyType() string { return "sdk/time.Time" }
func (t timeKey) EncodeNonTerminal(buffer []byte, key time.Time) (int, error) {
return t.Encode(buffer, key)
}

func (t timeKey) DecodeNonTerminal(buffer []byte) (int, time.Time, error) {
if len(buffer) < timeSize {
return 0, time.Time{}, fmt.Errorf("invalid time buffer size, wanted: %d at least, got: %d", timeSize, len(buffer))
}
return t.Decode(buffer[:timeSize])
}
func (t timeKey) SizeNonTerminal(key time.Time) int { return t.Size(key) }
7 changes: 5 additions & 2 deletions collections/collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package collections

import (
"context"
"cosmossdk.io/collections/codec"
"errors"
io "io"
"math"

"cosmossdk.io/collections/codec"
)

var (
Expand Down Expand Up @@ -51,6 +50,8 @@ var (
// BoolKey can be used to encode booleans. It uses a single byte to represent the boolean.
// 0x0 is used to represent false, and 0x1 is used to represent true.
BoolKey = codec.NewBoolKey[bool]()
// TimeKey can be used to encode time.Time keys. The encoding is done using the UnixMilli
TimeKey = codec.NewTimeKey()
)

// VALUES
Expand All @@ -72,6 +73,8 @@ var (
StringValue = codec.KeyToValueCodec(StringKey)
// BytesValue implements a ValueCodec for bytes.
BytesValue = codec.KeyToValueCodec(BytesKey)
// TimeValue implements a ValueCodec for time.Time.
TimeValue = codec.KeyToValueCodec(TimeKey)
)

// Collection is the interface that all collections implement. It will eventually
Expand Down
Loading