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

Slightly more efficient conversion from JSON to document #270

Merged
merged 5 commits into from
Oct 23, 2020
Merged
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
2 changes: 1 addition & 1 deletion database/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,7 @@ func validateConstraint(d document.Document, c *FieldConstraint) error {
}
case document.ArrayValue:
// if it's an array, we can assume it's a ValueBuffer
buf := parent.V.(*document.ValueBuffer)
buf := parent.V.(document.ValueBuffer)

frag := c.Path[len(c.Path)-1]
if frag.FieldName != "" {
Expand Down
7 changes: 6 additions & 1 deletion document/array.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ type ValueBuffer []Value

// NewValueBuffer creates a buffer of values.
func NewValueBuffer(values ...Value) ValueBuffer {
if len(values) == 0 {
// If called with no values return a non-nil slice.
return ValueBuffer{}
}

return ValueBuffer(values)
}

Expand Down Expand Up @@ -141,7 +146,7 @@ func (vb *ValueBuffer) Copy(a Array) error {
return err
}

err = vb.Replace(i, NewArrayValue(&buf))
err = vb.Replace(i, NewArrayValue(buf))
if err != nil {
return err
}
Expand Down
39 changes: 33 additions & 6 deletions document/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,43 @@ import (
"reflect"
"strings"
"time"

"github.com/buger/jsonparser"
)

// NewFromJSON creates a document from a JSON object.
func NewFromJSON(data []byte) (Document, error) {
var fb FieldBuffer
err := fb.UnmarshalJSON(data)
// NewFromJSON creates a document from raw JSON data.
// The returned document will lazily decode the data.
// If data is not a valid json object, calls to Iterate or GetByField will
// return an error.
func NewFromJSON(data []byte) Document {
return &jsonEncodedDocument{data}
}

type jsonEncodedDocument struct {
data []byte
}

func (j jsonEncodedDocument) Iterate(fn func(field string, value Value) error) error {
return jsonparser.ObjectEach(j.data, func(key, value []byte, dataType jsonparser.ValueType, offset int) error {
v, err := parseJSONValue(dataType, value)
if err != nil {
return err
}

return fn(string(key), v)
})
}

func (j jsonEncodedDocument) GetByField(field string) (Value, error) {
v, dt, _, err := jsonparser.Get(j.data, field)
if dt == jsonparser.NotExist {
return Value{}, ErrFieldNotFound
}
if err != nil {
return nil, err
return Value{}, err
}
return &fb, nil

return parseJSONValue(dt, v)
}

// NewFromMap creates a document from a map.
Expand Down
86 changes: 86 additions & 0 deletions document/create_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package document_test

import (
"testing"

"github.com/genjidb/genji/document"
"github.com/stretchr/testify/require"
)

func TestNewFromJSON(t *testing.T) {
tests := []struct {
name string
data string
expected *document.FieldBuffer
fails bool
}{
{"empty object", "{}", document.NewFieldBuffer(), false},
{"empty object, missing closing bracket", "{", nil, true},
{"classic object", `{"a": 1, "b": true, "c": "hello", "d": [1, 2, 3], "e": {"f": "g"}}`,
document.NewFieldBuffer().
Add("a", document.NewIntegerValue(1)).
Add("b", document.NewBoolValue(true)).
Add("c", document.NewTextValue("hello")).
Add("d", document.NewArrayValue(document.NewValueBuffer().
Append(document.NewIntegerValue(1)).
Append(document.NewIntegerValue(2)).
Append(document.NewIntegerValue(3)))).
Add("e", document.NewDocumentValue(document.NewFieldBuffer().Add("f", document.NewTextValue("g")))),
false},
{"string values", `{"a": "hello ciao"}`, document.NewFieldBuffer().Add("a", document.NewTextValue("hello ciao")), false},
{"+integer values", `{"a": 1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(1000)), false},
{"-integer values", `{"a": -1000}`, document.NewFieldBuffer().Add("a", document.NewIntegerValue(-1000)), false},
{"+float values", `{"a": 10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(10000000000)), false},
{"-float values", `{"a": -10000000000.0}`, document.NewFieldBuffer().Add("a", document.NewDoubleValue(-10000000000)), false},
{"bool values", `{"a": true, "b": false}`, document.NewFieldBuffer().Add("a", document.NewBoolValue(true)).Add("b", document.NewBoolValue(false)), false},
{"empty arrays", `{"a": []}`, document.NewFieldBuffer().Add("a", document.NewArrayValue(document.NewValueBuffer())), false},
{"nested arrays", `{"a": [[1, 2]]}`, document.NewFieldBuffer().
Add("a", document.NewArrayValue(
document.NewValueBuffer().
Append(document.NewArrayValue(
document.NewValueBuffer().
Append(document.NewIntegerValue(1)).
Append(document.NewIntegerValue(2)))))), false},
{"missing comma", `{"a": 1 "b": 2}`, nil, true},
{"missing closing brackets", `{"a": 1, "b": 2`, nil, true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
d := document.NewFromJSON([]byte(test.data))

fb := document.NewFieldBuffer()
err := fb.Copy(d)

if test.fails {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Equal(t, *test.expected, *fb)
}
})
}

t.Run("GetByField", func(t *testing.T) {
d := document.NewFromJSON([]byte(`{"a": 1000}`))

v, err := d.GetByField("a")
require.NoError(t, err)
require.Equal(t, document.NewIntegerValue(1000), v)

v, err = d.GetByField("b")
require.Equal(t, document.ErrFieldNotFound, err)
})
}

func BenchmarkJSONToDocument(b *testing.B) {
data := []byte(`{"_id":"5f8aefb8e443c6c13afdb305","index":0,"guid":"42c2719e-3371-4b2f-b855-d302a8b7eab0","isActive":true,"balance":"$1,064.79","picture":"http://placehold.it/32x32","age":40,"eyeColor":"blue","name":"Adele Webb","gender":"female","company":"EXTRAGEN","email":"adelewebb@extragen.com","phone":"+1 (964) 409-2397","address":"970 Charles Place, Watrous, Texas, 2522","about":"Amet non do ullamco duis velit sunt esse et cillum nisi mollit ea magna. Tempor ut occaecat proident laborum velit nisi et excepteur exercitation non est labore. Laboris pariatur enim proident et. Qui minim enim et incididunt incididunt adipisicing tempor. Occaecat adipisicing sint ex ut exercitation exercitation voluptate. Laboris adipisicing ut cillum eu cillum est sunt amet Lorem quis pariatur.\r\n","registered":"2016-05-25T10:36:44 -04:00","latitude":64.57112,"longitude":176.136138,"tags":["velit","minim","eiusmod","est","eu","voluptate","deserunt"],"friends":[{"id":0,"name":"Mathis Robertson"},{"id":1,"name":"Cecilia Donaldson"},{"id":2,"name":"Joann Goodwin"}],"greeting":"Hello, Adele Webb! You have 2 unread messages.","favoriteFruit":"apple"}`)

b.ResetTimer()
for i := 0; i < b.N; i++ {
d := document.NewFromJSON(data)
d.Iterate(func(string, document.Value) error {
return nil
})
}
}
2 changes: 1 addition & 1 deletion document/document.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func (fb *FieldBuffer) Copy(d Document) error {
return err
}

fb.fields[i].Value = NewArrayValue(&buf)
fb.fields[i].Value = NewArrayValue(buf)
}
}

Expand Down
6 changes: 2 additions & 4 deletions document/document_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ func TestFieldBuffer(t *testing.T) {
})

t.Run("Set", func(t *testing.T) {

tests := []struct {
name string
data string
Expand Down Expand Up @@ -118,9 +117,8 @@ func TestFieldBuffer(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
var fb document.FieldBuffer

d, err := document.NewFromJSON([]byte(tt.data))
require.NoError(t, err)
err = fb.Copy(d)
d := document.NewFromJSON([]byte(tt.data))
err := fb.Copy(d)
require.NoError(t, err)
p, err := parser.ParsePath(tt.path)
require.NoError(t, err)
Expand Down
4 changes: 1 addition & 3 deletions sql/query/expr/expr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@ import (
)

var doc document.Document = func() document.Document {
d, _ := document.NewFromJSON([]byte(`{
return document.NewFromJSON([]byte(`{
"a": 1,
"b": {"foo bar": [1, 2]},
"c": [1, {"foo": "bar"}, [1, 2]]
}`))

return d
}()

var stackWithDoc = expr.EvalStack{
Expand Down
10 changes: 6 additions & 4 deletions sql/query/expr/path_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package expr_test

import (
"encoding/json"
"testing"

"github.com/genjidb/genji/document"
Expand All @@ -16,8 +17,10 @@ func TestPathExpr(t *testing.T) {
}{
{"a", document.NewIntegerValue(1), false},
{"b", func() document.Value {
d, _ := document.NewFromJSON([]byte(`{"foo bar": [1, 2]}`))
return document.NewDocumentValue(d)
fb := document.NewFieldBuffer()
err := json.Unmarshal([]byte(`{"foo bar": [1, 2]}`), fb)
require.NoError(t, err)
return document.NewDocumentValue(fb)
}(),
false},
{"b.`foo bar`[0]", document.NewIntegerValue(1), false},
Expand All @@ -30,12 +33,11 @@ func TestPathExpr(t *testing.T) {
{"d", nullLitteral, false},
}

d, err := document.NewFromJSON([]byte(`{
d := document.NewFromJSON([]byte(`{
"a": 1,
"b": {"foo bar": [1, 2]},
"c": [1, {"foo": "bar"}, [1, 2]]
}`))
require.NoError(t, err)

for _, test := range tests {
t.Run(test.expr, func(t *testing.T) {
Expand Down