Skip to content

Commit

Permalink
stop number normalization for concurrent execution with same data (fix
Browse files Browse the repository at this point in the history
  • Loading branch information
itchyny committed Apr 9, 2023
1 parent 36b374f commit 307bb56
Show file tree
Hide file tree
Showing 18 changed files with 100 additions and 146 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func main() {
- Firstly, use [`gojq.Parse(string) (*Query, error)`](https://pkg.go.dev/github.com/itchyny/gojq#Parse) to get the query from a string.
- Secondly, get the result iterator
- using [`query.Run`](https://pkg.go.dev/github.com/itchyny/gojq#Query.Run) or [`query.RunWithContext`](https://pkg.go.dev/github.com/itchyny/gojq#Query.RunWithContext)
- or alternatively, compile the query using [`gojq.Compile`](https://pkg.go.dev/github.com/itchyny/gojq#Compile) and then [`code.Run`](https://pkg.go.dev/github.com/itchyny/gojq#Code.Run) or [`code.RunWithContext`](https://pkg.go.dev/github.com/itchyny/gojq#Code.RunWithContext). You can reuse the `*Code` against multiple inputs to avoid compilation of the same query. But for arguments of `code.Run`, do not give values sharing same data between multiple calls.
- or alternatively, compile the query using [`gojq.Compile`](https://pkg.go.dev/github.com/itchyny/gojq#Compile) and then [`code.Run`](https://pkg.go.dev/github.com/itchyny/gojq#Code.Run) or [`code.RunWithContext`](https://pkg.go.dev/github.com/itchyny/gojq#Code.RunWithContext). You can reuse the `*Code` against multiple inputs to avoid compilation of the same query.
- In either case, you cannot use custom type values as the query input. The type should be `[]any` for an array and `map[string]any` for a map (just like decoded to an `any` using the [encoding/json](https://golang.org/pkg/encoding/json/) package). You can't use `[]int` or `map[string]string`, for example. If you want to query your custom struct, marshal to JSON, unmarshal to `any` and use it as the query input.
- Thirdly, iterate through the results using [`iter.Next() (any, bool)`](https://pkg.go.dev/github.com/itchyny/gojq#Iter). The iterator can emit an error so make sure to handle it. The method returns `true` with results, and `false` when the iterator terminates.
- The return type is not `(any, error)` because iterators can emit multiple errors and you can continue after an error. It is difficult for the iterator to tell the termination in this situation.
Expand Down
3 changes: 3 additions & 0 deletions cli/encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cli

import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
Expand Down Expand Up @@ -56,6 +57,8 @@ func (e *encoder) encode(v any) error {
e.encodeFloat64(v)
case *big.Int:
e.write(v.Append(e.buf[:0], 10), numberColor)
case json.Number:
e.write([]byte(v.String()), numberColor)
case string:
e.encodeString(v, stringColor)
case []any:
Expand Down
11 changes: 10 additions & 1 deletion cli/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
- name: number input
args:
- '.'
input: '128'
input: '128 3.14 10.0'
expected: |
128
3.14
10.0
- name: number query
args:
Expand Down Expand Up @@ -282,6 +284,13 @@
expected: |
4722366482869645213696
- name: object indexing with floating-point number with trailing zeros
args:
- '.foo'
input: '{"foo": 100.000000}'
expected: |
100.000000
- name: object indexing by keywords
args:
- '.and,.or,.try'
Expand Down
3 changes: 2 additions & 1 deletion compare.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gojq

import (
"encoding/json"
"math"
"math/big"
)
Expand Down Expand Up @@ -88,7 +89,7 @@ func typeIndex(v any) int {
return 1
}
return 2
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
return 3
case string:
return 4
Expand Down
9 changes: 2 additions & 7 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ type Code struct {
// a result iterator.
//
// It is safe to call this method in goroutines, to reuse a compiled [*Code].
// But for arguments, do not give values sharing same data between goroutines.
func (c *Code) Run(v any, values ...any) Iter {
return c.RunWithContext(context.Background(), v, values...)
}
Expand All @@ -46,10 +45,7 @@ func (c *Code) RunWithContext(ctx context.Context, v any, values ...any) Iter {
} else if len(values) < len(c.variables) {
return NewIter(&expectedVariableError{c.variables[len(values)]})
}
for i, v := range values {
values[i] = normalizeNumbers(v)
}
return newEnv(ctx).execute(c, normalizeNumbers(v), values...)
return newEnv(ctx).execute(c, v, values...)
}

type scopeinfo struct {
Expand Down Expand Up @@ -160,7 +156,6 @@ func (c *compiler) compileImport(i *Import) error {
} else {
return fmt.Errorf("module not found: %q", path)
}
vals = normalizeNumbers(vals)
c.append(&code{op: oppush, v: vals})
c.append(&code{op: opstore, v: c.pushVariable(alias)})
c.append(&code{op: oppush, v: vals})
Expand Down Expand Up @@ -1166,7 +1161,7 @@ func (c *compiler) funcInput(any, []any) any {
if !ok {
return errors.New("break")
}
return normalizeNumbers(v)
return v
}

func (c *compiler) funcModulemeta(v any, _ []any) any {
Expand Down
15 changes: 9 additions & 6 deletions encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gojq

import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
Expand All @@ -15,12 +16,12 @@ import (
// Marshal returns the jq-flavored JSON encoding of v.
//
// This method accepts only limited types (nil, bool, int, float64, *big.Int,
// string, []any, and map[string]any) because these are the possible types a
// gojq iterator can emit. This method marshals NaN to null, truncates
// infinities to (+|-) math.MaxFloat64, uses \b and \f in strings, and does not
// escape '<', '>', '&', '\u2028', and '\u2029'. These behaviors are based on
// the marshaler of jq command, and different from json.Marshal in the Go
// standard library. Note that the result is not safe to embed in HTML.
// json.Number, string, []any, and map[string]any) because these are the
// possible types a gojq iterator can emit. This method marshals NaN to null,
// truncates infinities to (+|-) math.MaxFloat64, uses \b and \f in strings,
// and does not escape '<', '>', '&', '\u2028', and '\u2029'. These behaviors
// are based on the marshaler of jq command, and different from json.Marshal in
// the Go standard library. Note that the result is not safe to embed in HTML.
func Marshal(v any) ([]byte, error) {
var b bytes.Buffer
(&encoder{w: &b}).encode(v)
Expand Down Expand Up @@ -62,6 +63,8 @@ func (e *encoder) encode(v any) {
e.encodeFloat64(v)
case *big.Int:
e.w.Write(v.Append(e.buf[:0], 10))
case json.Number:
e.w.WriteString(v.String())
case string:
e.encodeString(v)
case []any:
Expand Down
5 changes: 3 additions & 2 deletions encoder_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package gojq_test

import (
"encoding/json"
"fmt"
"math"
"math/big"
Expand All @@ -24,10 +25,10 @@ func TestMarshal(t *testing.T) {
},
{
value: []any{
42, 3.14, 1e-6, 1e-7, -1e-9, 1e-10, math.NaN(), math.Inf(1), math.Inf(-1),
42, 3.14, 1e-6, 1e-7, -1e-9, 1e-10, math.NaN(), math.Inf(1), math.Inf(-1), json.Number("42"),
new(big.Int).SetBytes([]byte("\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff")),
},
expected: "[42,3.14,0.000001,1e-7,-1e-9,1e-10,null,1.7976931348623157e+308,-1.7976931348623157e+308,340282366920938463463374607431768211455]",
expected: "[42,3.14,0.000001,1e-7,-1e-9,1e-10,null,1.7976931348623157e+308,-1.7976931348623157e+308,42,340282366920938463463374607431768211455]",
},
{
value: []any{"", "abcde", "foo\x00\x1f\r\n\t\f\b<=>!\"#$%'& \\\x7fbar"},
Expand Down
42 changes: 35 additions & 7 deletions func.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,11 @@ func funcLength(v any) any {
return v
}
return new(big.Int).Abs(v)
case json.Number:
if strings.HasPrefix(v.String(), "-") {
return v[1:]
}
return v
case string:
return len([]rune(v))
case []any:
Expand Down Expand Up @@ -490,7 +495,7 @@ func funcAdd(v any) any {

func funcToNumber(v any) any {
switch v := v.(type) {
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
return v
case string:
if !newLexer(v).validNumber() {
Expand All @@ -503,7 +508,7 @@ func funcToNumber(v any) any {
}

func toNumber(v string) any {
return normalizeNumber(json.Number(v))
return parseNumber(json.Number(v))
}

func funcToString(v any) any {
Expand Down Expand Up @@ -794,7 +799,7 @@ func funcFromJSON(v any) any {
if _, err := dec.Token(); err != io.EOF {
return &funcTypeError{"fromjson", v}
}
return normalizeNumbers(w)
return w
}

func funcFormat(v, x any) any {
Expand Down Expand Up @@ -930,7 +935,7 @@ func funcIndex2(_, v, x any) any {
default:
return &expectedObjectError{v}
}
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
i, _ := toInt(x)
switch v := v.(type) {
case nil:
Expand Down Expand Up @@ -1133,7 +1138,7 @@ func (iter *rangeIter) Next() (any, bool) {
func funcRange(_ any, xs []any) any {
for _, x := range xs {
switch x.(type) {
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
default:
return &funcTypeError{"range", x}
}
Expand Down Expand Up @@ -1310,7 +1315,7 @@ func funcJoin(v, x any) any {
} else {
ss[i] = "false"
}
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
ss[i] = jsonMarshal(v)
default:
return &joinTypeError{v}
Expand Down Expand Up @@ -1527,7 +1532,7 @@ func update(v any, path []any, n any, a allocator) (any, error) {
default:
return nil, &expectedObjectError{v}
}
case int, float64, *big.Int:
case int, float64, *big.Int, json.Number:
i, _ := toInt(p)
switch v := v.(type) {
case nil:
Expand Down Expand Up @@ -2063,6 +2068,8 @@ func toInt(x any) (int, bool) {
return math.MaxInt, true
}
return math.MinInt, true
case json.Number:
return toInt(parseNumber(x))
default:
return 0, false
}
Expand All @@ -2086,6 +2093,9 @@ func toFloat(x any) (float64, bool) {
return x, true
case *big.Int:
return bigToFloat(x), true
case json.Number:
v, _ := x.Float64()
return v, true
default:
return 0.0, false
}
Expand All @@ -2100,3 +2110,21 @@ func bigToFloat(x *big.Int) float64 {
}
return math.Inf(x.Sign())
}

func parseNumber(v json.Number) any {
if i, err := v.Int64(); err == nil && math.MinInt <= i && i <= math.MaxInt {
return int(i)
}
if strings.ContainsAny(v.String(), ".eE") {
if f, err := v.Float64(); err == nil {
return f
}
}
if bi, ok := new(big.Int).SetString(v.String(), 10); ok {
return bi
}
if strings.HasPrefix(v.String(), "-") {
return math.Inf(-1)
}
return math.Inf(1)
}
84 changes: 0 additions & 84 deletions normalize.go

This file was deleted.

0 comments on commit 307bb56

Please sign in to comment.