Skip to content

Commit

Permalink
types: Add UnixNano based on UnixMilli
Browse files Browse the repository at this point in the history
As Icinga/icinga-notifications#61 required a more precise time type, the
UnixMilli type was copied to UnixNano, representing nanoseconds.

While implementing the relevant changes, some changes were backported to
UnixMilli. For example, the util.FromUnixMilli function is no longer
used and was marked as deprecated as there is now time.UnixMilli in the
standard library.
  • Loading branch information
oxzi committed Apr 18, 2024
1 parent 1e3dea1 commit 0aa3f56
Show file tree
Hide file tree
Showing 4 changed files with 132 additions and 11 deletions.
11 changes: 4 additions & 7 deletions pkg/types/unix_milli.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ import (
"encoding"
"encoding/json"
"github.com/icinga/icingadb/internal"
"github.com/icinga/icingadb/pkg/utils"
"github.com/pkg/errors"
"strconv"
"time"
)

// UnixMilli is a nullable millisecond UNIX timestamp in databases and JSON.
// UnixMilli is a nullable millisecond Unix timestamp in databases and JSON.
type UnixMilli time.Time

// Time returns the time.Time conversion of UnixMilli.
Expand All @@ -37,7 +36,7 @@ func (t *UnixMilli) UnmarshalText(text []byte) error {
return internal.CantParseFloat64(err, string(text))
}

*t = UnixMilli(utils.FromUnixMilli(int64(parsed)))
*t = UnixMilli(time.UnixMilli(int64(parsed)))
return nil
}

Expand All @@ -52,8 +51,7 @@ func (t *UnixMilli) UnmarshalJSON(data []byte) error {
if err != nil {
return internal.CantParseFloat64(err, string(data))
}
tt := utils.FromUnixMilli(int64(ms))
*t = UnixMilli(tt)
*t = UnixMilli(time.UnixMilli(int64(ms)))

return nil
}
Expand All @@ -69,8 +67,7 @@ func (t *UnixMilli) Scan(src interface{}) error {
if !ok {
return errors.Errorf("bad int64 type assertion from %#v", src)
}
tt := utils.FromUnixMilli(v)
*t = UnixMilli(tt)
*t = UnixMilli(time.UnixMilli(v))

return nil
}
Expand Down
95 changes: 95 additions & 0 deletions pkg/types/unix_nano.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package types

import (
"database/sql"
"database/sql/driver"
"encoding"
"encoding/json"
"github.com/icinga/icingadb/internal"
"github.com/pkg/errors"
"strconv"
"time"
)

// UnixNano is a nullable nanosecond Unix timestamp in databases and JSON.
//
// Please be aware that according to Time.UnixNano's documentation the internal int64 cannot hold a date before the year
// 1678 or after 2262. Using UnixNano creates a Year 2262 problem, unless we will have ditched int64 or gone extinct.
type UnixNano time.Time

// Time returns the time.Time conversion of UnixNano.
func (t UnixNano) Time() time.Time {
return time.Time(t)
}

// MarshalJSON implements the json.Marshaler interface.
// Marshals to nanoseconds. Supports JSON null.
func (t UnixNano) MarshalJSON() ([]byte, error) {
if time.Time(t).IsZero() {
return []byte("null"), nil
}

return []byte(strconv.FormatInt(time.Time(t).UnixNano(), 10)), nil
}

// UnmarshalText implements the encoding.TextUnmarshaler interface.
func (t *UnixNano) UnmarshalText(text []byte) error {
parsed, err := strconv.ParseFloat(string(text), 64)
if err != nil {
return internal.CantParseFloat64(err, string(text))
}

*t = UnixNano(time.Unix(0, int64(parsed)))
return nil
}

// UnmarshalJSON implements the json.Unmarshaler interface.
// Unmarshals from nanoseconds. Supports JSON null.
func (t *UnixNano) UnmarshalJSON(data []byte) error {
if string(data) == "null" || len(data) == 0 {
return nil
}

ns, err := strconv.ParseFloat(string(data), 64)
if err != nil {
return internal.CantParseFloat64(err, string(data))
}
*t = UnixNano(time.Unix(0, int64(ns)))

return nil
}

// Scan implements the sql.Scanner interface.
// Scans from nanoseconds. Supports SQL NULL.
func (t *UnixNano) Scan(src interface{}) error {
if src == nil {
return nil
}

v, ok := src.(int64)
if !ok {
return errors.Errorf("bad int64 type assertion from %#v", src)
}
*t = UnixNano(time.Unix(0, v))

return nil
}

// Value implements the driver.Valuer interface.
// Returns nanoseconds. Supports SQL NULL.
func (t UnixNano) Value() (driver.Value, error) {
if t.Time().IsZero() {
return nil, nil
}

return t.Time().UnixNano(), nil
}

// Assert interface compliance.
var (
_ json.Marshaler = (*UnixNano)(nil)
_ encoding.TextUnmarshaler = (*UnixNano)(nil)
_ json.Unmarshaler = (*UnixNano)(nil)
_ sql.Scanner = (*UnixNano)(nil)
_ driver.Valuer = (*UnixNano)(nil)
)
30 changes: 30 additions & 0 deletions pkg/types/unix_nano_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package types

import (
"github.com/stretchr/testify/require"
"testing"
"time"
"unicode/utf8"
)

func TestUnixNano_MarshalJSON(t *testing.T) {
subtests := []struct {
name string
input UnixNano
output string
}{
{"zero", UnixNano{}, `null`},
{"epoch", UnixNano(time.Unix(0, 0)), `0`},
{"nonzero", UnixNano(time.Unix(1234567890, 62500000)), `1234567890062500000`},
}

for _, st := range subtests {
t.Run(st.name, func(t *testing.T) {
actual, err := st.input.MarshalJSON()

require.NoError(t, err)
require.True(t, utf8.Valid(actual))
require.Equal(t, st.output, string(actual))
})
}
}
7 changes: 3 additions & 4 deletions pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/lib/pq"
"github.com/pkg/errors"
"golang.org/x/exp/utf8string"
"math"
"net"
"os"
"path/filepath"
Expand All @@ -20,10 +19,10 @@ import (

// FromUnixMilli creates and returns a time.Time value
// from the given milliseconds since the Unix epoch ms.
//
// Deprecated: Use time.UnixMilli instead.
func FromUnixMilli(ms int64) time.Time {
sec, dec := math.Modf(float64(ms) / 1e3)

return time.Unix(int64(sec), int64(dec*(1e9)))
return time.UnixMilli(ms)
}

// Name returns the declared name of type t.
Expand Down

0 comments on commit 0aa3f56

Please sign in to comment.