Skip to content

Commit

Permalink
feat: Enable indexing for DateTime fields (#2933)
Browse files Browse the repository at this point in the history
## Relevant issue(s)

Resolves #2914

## Description

Make indexes handle time.Time type as well.
For this encoding/decoding of time type is added to encoding package.
  • Loading branch information
islamaliev committed Aug 20, 2024
1 parent 9422244 commit 373f90b
Show file tree
Hide file tree
Showing 9 changed files with 502 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ require (
github.com/multiformats/go-multicodec v0.9.0
github.com/multiformats/go-multihash v0.2.3
github.com/pelletier/go-toml v1.9.5
github.com/pkg/errors v0.9.1
github.com/sourcenetwork/acp_core v0.0.0-20240607160510-47a5306b2ad2
github.com/sourcenetwork/badger/v4 v4.2.1-0.20231113215945-a63444ca5276
github.com/sourcenetwork/corelog v0.0.8
Expand Down Expand Up @@ -304,7 +305,6 @@ require (
github.com/pion/turn/v2 v2.1.6 // indirect
github.com/pion/webrtc/v3 v3.2.50 // indirect
github.com/piprate/json-gold v0.5.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/polydawn/refmt v0.89.0 // indirect
github.com/pquerna/cachecontrol v0.1.0 // indirect
Expand Down
38 changes: 38 additions & 0 deletions internal/db/fetcher/indexer_iterators.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"context"
"errors"
"strings"
"time"

ds "github.com/ipfs/go-datastore"

Expand Down Expand Up @@ -331,6 +332,37 @@ func (m *stringMatcher) Match(value client.NormalValue) (bool, error) {
return false, NewErrUnexpectedTypeValue[string](value)
}

type timeMatcher struct {
op string
value time.Time
}

func (m *timeMatcher) Match(value client.NormalValue) (bool, error) {
timeVal, ok := value.Time()
if !ok {
if timeOptVal, ok := value.NillableTime(); ok {
timeVal = timeOptVal.Value()
} else {
return false, NewErrUnexpectedTypeValue[time.Time](value)
}
}
switch m.op {
case opEq:
return timeVal.Equal(m.value), nil
case opGt:
return timeVal.After(m.value), nil
case opGe:
return !timeVal.Before(m.value), nil
case opLt:
return timeVal.Before(m.value), nil
case opLe:
return !timeVal.After(m.value), nil
case opNe:
return !timeVal.Equal(m.value), nil
}
return false, NewErrInvalidFilterOperator(m.op)
}

type nilMatcher struct {
matchNil bool
}
Expand Down Expand Up @@ -608,6 +640,12 @@ func createValueMatcher(condition *fieldFilterCond) (valueMatcher, error) {
if v, ok := condition.val.NillableString(); ok {
return &stringMatcher{value: v.Value(), evalFunc: getCompareValsFunc[string](condition.op)}, nil
}
if v, ok := condition.val.Time(); ok {
return &timeMatcher{value: v, op: condition.op}, nil
}
if v, ok := condition.val.NillableTime(); ok {
return &timeMatcher{value: v.Value(), op: condition.op}, nil
}
case opIn, opNin:
inVals, err := client.ToArrayOfNormalValues(condition.val)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/encoding/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
floatNaNDesc
bytesMarker
bytesDescMarker
timeMarker

// These constants define a range of values and are used to determine how many bytes are
// needed to represent the given uint64 value. The constants IntMin and IntMax define the
Expand Down
26 changes: 26 additions & 0 deletions internal/encoding/field_value.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
package encoding

import (
"time"

"github.com/sourcenetwork/defradb/client"
)

Expand Down Expand Up @@ -80,6 +82,18 @@ func EncodeFieldValue(b []byte, val client.NormalValue, descending bool) []byte
}
return EncodeStringAscending(b, v.Value())
}
if v, ok := val.Time(); ok {
if descending {
return EncodeTimeDescending(b, v)
}
return EncodeTimeAscending(b, v)
}
if v, ok := val.NillableTime(); ok {
if descending {
return EncodeTimeDescending(b, v.Value())
}
return EncodeTimeAscending(b, v.Value())
}

return b
}
Expand Down Expand Up @@ -129,6 +143,18 @@ func DecodeFieldValue(b []byte, descending bool, kind client.FieldKind) ([]byte,
return nil, nil, NewErrCanNotDecodeFieldValue(b, kind, err)
}
return b, client.NewNormalString(v), nil
case Time:
var v time.Time
var err error
if descending {
b, v, err = DecodeTimeDescending(b)
} else {
b, v, err = DecodeTimeAscending(b)
}
if err != nil {
return nil, nil, NewErrCanNotDecodeFieldValue(b, kind, err)
}
return b, client.NewNormalTime(v), nil
}

return nil, nil, NewErrCanNotDecodeFieldValue(b, kind)
Expand Down
78 changes: 78 additions & 0 deletions internal/encoding/time.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 encoding

import (
"time"

"github.com/pkg/errors"
)

// EncodeTimeAscending encodes a time value, appends it to the supplied buffer,
// and returns the final buffer. The encoding is guaranteed to be ordered
// Such that if t1.Before(t2) then after EncodeTime(b1, t1), and
// EncodeTime(b2, t2), Compare(b1, b2) < 0. The time zone offset not
// included in the encoding.
func EncodeTimeAscending(b []byte, t time.Time) []byte {
return encodeTime(b, t.Unix(), int64(t.Nanosecond()))
}

// EncodeTimeDescending is the descending version of EncodeTimeAscending.
func EncodeTimeDescending(b []byte, t time.Time) []byte {
return encodeTime(b, ^t.Unix(), ^int64(t.Nanosecond()))
}

func encodeTime(b []byte, unix, nanos int64) []byte {
// Read the unix absolute time. This is the absolute time and is
// not time zone offset dependent.
b = append(b, timeMarker)
b = EncodeVarintAscending(b, unix)
b = EncodeVarintAscending(b, nanos)
return b
}

// DecodeTimeAscending decodes a time.Time value which was encoded using
// EncodeTime. The remainder of the input buffer and the decoded
// time.Time are returned.
func DecodeTimeAscending(b []byte) ([]byte, time.Time, error) {
b, sec, nsec, err := decodeTime(b)
if err != nil {
return b, time.Time{}, err
}
return b, time.Unix(sec, nsec).UTC(), nil
}

// DecodeTimeDescending is the descending version of DecodeTimeAscending.
func DecodeTimeDescending(b []byte) ([]byte, time.Time, error) {
b, sec, nsec, err := decodeTime(b)
if err != nil {
return b, time.Time{}, err
}
return b, time.Unix(^sec, ^nsec).UTC(), nil
}

func decodeTime(b []byte) (r []byte, sec int64, nsec int64, err error) {
if PeekType(b) != Time {
return nil, 0, 0, errors.Errorf("did not find marker")
}
b = b[1:]
b, sec, err = DecodeVarintAscending(b)
if err != nil {
return b, 0, 0, err
}
b, nsec, err = DecodeVarintAscending(b)
if err != nil {
return b, 0, 0, err
}
return b, sec, nsec, nil
}
3 changes: 3 additions & 0 deletions internal/encoding/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
Float Type = 4
Bytes Type = 6
BytesDesc Type = 7
Time Type = 8
)

// PeekType peeks at the type of the value encoded at the start of b.
Expand All @@ -40,6 +41,8 @@ func PeekType(b []byte) Type {
return Int
case m >= floatNaN && m <= floatNaNDesc:
return Float
case m == timeMarker:
return Time
}
}
return Unknown
Expand Down
32 changes: 32 additions & 0 deletions tests/integration/index/create_unique_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,3 +315,35 @@ func TestUniqueIndexCreate_UponAddingDocWithExistingNilValue_ShouldSucceed(t *te

testUtils.ExecuteTestCase(t, test)
}

func TestUniqueQueryWithIndex_UponAddingDocWithSameDateTime_Error(t *testing.T) {
test := testUtils.TestCase{
Actions: []any{
testUtils.SchemaUpdate{
Schema: `
type User {
name: String
birthday: DateTime @index(unique: true)
}`,
},
testUtils.CreateDoc{
Doc: `{
"name": "Fred",
"birthday": "2000-07-23T03:00:00-00:00"
}`,
},
testUtils.CreateDoc{
Doc: `{
"name": "Andy",
"birthday": "2000-07-23T03:00:00-00:00"
}`,
ExpectedError: db.NewErrCanNotIndexNonUniqueFields(
"bae-2000529a-8b27-539b-91e9-c35f431fb78e",
errors.NewKV("birthday", testUtils.MustParseTime("2000-07-23T03:00:00-00:00")),
).Error(),
},
},
}

testUtils.ExecuteTestCase(t, test)
}
Loading

0 comments on commit 373f90b

Please sign in to comment.