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

tjson: Implement datetime #1027

Merged
merged 16 commits into from
Aug 12, 2022
4 changes: 2 additions & 2 deletions integration/shareddata/shareddata.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ func AllProviders() []Provider {
Binaries,
ObjectIDs,
Bools,
// DateTimes, TODO https://github.com/FerretDB/FerretDB/issues/905
DateTimes,
// Nulls, TODO https://github.com/FerretDB/FerretDB/issues/906
// Regexes, TODO https://github.com/FerretDB/FerretDB/issues/911
Int32s,
// Timestamps, TODO https://github.com/FerretDB/FerretDB/issues/905
// Timestamps, TODO https://github.com/FerretDB/FerretDB/issues/1007
Int64s,
// Unsets, TODO https://github.com/FerretDB/FerretDB/issues/1023

Expand Down
72 changes: 72 additions & 0 deletions internal/tjson/datetime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tjson

import (
"bytes"
"encoding/json"
"time"

"github.com/FerretDB/FerretDB/internal/util/lazyerrors"
)

// dateTimeType represents BSON UTC datetime type.
type dateTimeType time.Time

// tjsontype implements tjsontype interface.
func (dt *dateTimeType) tjsontype() {}

noisersup marked this conversation as resolved.
Show resolved Hide resolved
// String returns formatted time for debugging.
func (dt *dateTimeType) String() string {
return time.Time(*dt).Format(time.RFC3339Nano)
}

// UnmarshalJSON implements the json.Unmarshaler interface.
func (dt *dateTimeType) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("null")) {
panic("null data")
}

r := bytes.NewReader(data)
dec := json.NewDecoder(r)

var o time.Time
if err := dec.Decode(&o); err != nil {
return lazyerrors.Error(err)
}

if err := checkConsumed(dec, r); err != nil {
return lazyerrors.Error(err)
}

*dt = dateTimeType(time.UnixMilli(o.UnixMilli()).UTC())

return nil
}

// MarshalJSON implements tjsontype interface.
func (dt *dateTimeType) MarshalJSON() ([]byte, error) {
res, err := json.Marshal(time.UnixMilli(time.Time(*dt).UnixMilli()).UTC())
if err != nil {
return nil, lazyerrors.Error(err)
}

return res, nil
}

// check interfaces
var (
_ tjsontype = (*dateTimeType)(nil)
)
62 changes: 62 additions & 0 deletions internal/tjson/datetime_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright 2021 FerretDB Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package tjson

import (
"testing"
"time"

"github.com/AlekSi/pointer"
)

var dateTimeTestCases = []testCase{{
name: "2021",
v: pointer.To(dateTimeType(time.Date(2021, 11, 1, 10, 18, 42, 123000000, time.UTC))),
schema: dateTimeSchema,
j: `"2021-11-01T10:18:42.123Z"`,
}, {
name: "unix_zero",
v: pointer.To(dateTimeType(time.Unix(0, 0).UTC())),
schema: dateTimeSchema,
j: `"1970-01-01T00:00:00Z"`,
}, {
name: "0",
v: pointer.To(dateTimeType(time.Date(0, 1, 1, 0, 0, 0, 0, time.UTC))),
schema: dateTimeSchema,
j: `"0000-01-01T00:00:00Z"`,
}, {
name: "9999",
v: pointer.To(dateTimeType(time.Date(9999, 12, 31, 23, 59, 59, 999000000, time.UTC))),
schema: dateTimeSchema,
j: `"9999-12-31T23:59:59.999Z"`,
}, {
name: "EOF",
schema: stringSchema,
j: `{`,
jErr: `unexpected EOF`,
}}

func TestDateTime(t *testing.T) {
t.Parallel()
testJSON(t, dateTimeTestCases, func() tjsontype { return new(dateTimeType) })
}

func FuzzDateTime(f *testing.F) {
fuzzJSON(f, dateTimeTestCases)
}

func BenchmarkDateTime(b *testing.B) {
benchmark(b, dateTimeTestCases)
}
3 changes: 1 addition & 2 deletions internal/tjson/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,8 +267,7 @@ func valueSchema(v any) (*Schema, error) {
case bool:
return boolSchema, nil
case time.Time:
// return dateTimeSchema, nil
return nil, lazyerrors.Errorf("%T is not supported yet", v)
return dateTimeSchema, nil
case types.NullType:
return nil, lazyerrors.Errorf("%T is not supported yet", v)
case types.Regex:
Expand Down
15 changes: 10 additions & 5 deletions internal/tjson/tjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"io"
"time"

"github.com/AlekSi/pointer"

Expand Down Expand Up @@ -70,8 +71,8 @@ func fromTJSON(v tjsontype) any {
return types.ObjectID(*v)
case *boolType:
return bool(*v)
// case *dateTimeType:
// return time.Time(*v)
case *dateTimeType:
return time.Time(*v)
// case *nullType:
// return types.Null
// case *regexType:
Expand Down Expand Up @@ -104,8 +105,8 @@ func toTJSON(v any) tjsontype {
return pointer.To(objectIDType(v))
case bool:
return pointer.To(boolType(v))
// case time.Time:
// return pointer.To(dateTimeType(v))
case time.Time:
return pointer.To(dateTimeType(v))
// case types.NullType:
// return pointer.To(nullType(v))
// case types.Regex:
Expand Down Expand Up @@ -168,7 +169,11 @@ func Unmarshal(data []byte, schema *Schema) (any, error) {
var o objectIDType
err = o.UnmarshalJSON(data)
res = &o
case UUID, DateTime:
case DateTime:
var o dateTimeType
err = o.UnmarshalJSON(data)
res = &o
case UUID:
fallthrough
case Double, Float, Int64, Int32:
fallthrough
Expand Down
2 changes: 2 additions & 0 deletions internal/tjson/tjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,8 @@ func unmarshalJSON(v tjsontype, j string) (bool, error) {
err = v.UnmarshalJSON([]byte(j))
case *boolType:
err = v.UnmarshalJSON([]byte(j))
case *dateTimeType:
err = v.UnmarshalJSON([]byte(j))
case *int32Type:
err = v.UnmarshalJSON([]byte(j))
case *int64Type:
Expand Down