diff --git a/enginetest/memory_engine_test.go b/enginetest/memory_engine_test.go index bd84aae5d2..415bc4bf83 100644 --- a/enginetest/memory_engine_test.go +++ b/enginetest/memory_engine_test.go @@ -847,6 +847,12 @@ func TestJsonScripts(t *testing.T) { enginetest.TestJsonScripts(t, enginetest.NewDefaultMemoryHarness(), skippedTests) } +// TestJsonScriptsPrepared runs JSON scripts through prepared execution. +func TestJsonScriptsPrepared(t *testing.T) { + var skippedTests []string = nil + enginetest.TestJsonScriptsPrepared(t, enginetest.NewDefaultMemoryHarness(), skippedTests) +} + func TestShowTableStatus(t *testing.T) { enginetest.TestShowTableStatus(t, enginetest.NewDefaultMemoryHarness()) } diff --git a/enginetest/queries/json_scripts.go b/enginetest/queries/json_scripts.go index 6d52afebd3..4147857212 100644 --- a/enginetest/queries/json_scripts.go +++ b/enginetest/queries/json_scripts.go @@ -24,6 +24,28 @@ import ( ) var JsonScripts = []ScriptTest{ + { + Name: "typed decimal remains exact in JSON", + SetUpScript: []string{ + "CREATE TABLE json_precision (id INT PRIMARY KEY, doc JSON)", + `INSERT INTO json_precision VALUES (1, '{"value":0}')`, + `UPDATE json_precision SET doc = JSON_SET(doc, '$.value', CAST(1234567890.123456789 AS DECIMAL(30,18))) WHERE id = 1`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT JSON_UNQUOTE(JSON_EXTRACT(doc, '$.value')) FROM json_precision WHERE id = 1`, + Expected: []sql.Row{{"1234567890.123456789000000000"}}, + }, + { + Query: `SELECT JSON_OVERLAPS(JSON_EXTRACT(doc, '$.value'), JSON_EXTRACT(JSON_SET('{}', '$.n', CAST(1234567890.123456789 AS DECIMAL(30,18))), '$.n')), JSON_OVERLAPS(JSON_EXTRACT(doc, '$.value'), JSON_EXTRACT(JSON_SET('{}', '$.n', CAST(1234567890.123456788 AS DECIMAL(30,18))), '$.n')) FROM json_precision WHERE id = 1`, + Expected: []sql.Row{{true, false}}, + }, + { + Query: `SELECT JSON_CONTAINS(doc, JSON_EXTRACT(JSON_SET('{}', '$.n', CAST(1234567890.123456789 AS DECIMAL(30,18))), '$.n'), '$.value'), JSON_CONTAINS(doc, JSON_EXTRACT(JSON_SET('{}', '$.n', CAST(1234567890.123456788 AS DECIMAL(30,18))), '$.n'), '$.value') FROM json_precision WHERE id = 1`, + Expected: []sql.Row{{true, false}}, + }, + }, + }, { // https://github.com/dolthub/dolt/issues/10050 Name: "TextStorage converts to JSON when using dolt wrapper", diff --git a/sql/expression/function/json/json_overlaps.go b/sql/expression/function/json/json_overlaps.go index a97c562a79..afbf26c957 100644 --- a/sql/expression/function/json/json_overlaps.go +++ b/sql/expression/function/json/json_overlaps.go @@ -15,8 +15,11 @@ package json import ( + "context" "fmt" + "github.com/cockroachdb/apd/v3" + "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" ) @@ -83,6 +86,11 @@ func (j *JSONOverlaps) IsNullable(ctx *sql.Context) bool { // It returns true if the two values are exactly equal (type and order are important). // It will recursively unwrap arrays and objects to compare their contents. func jsonEquals(left, right interface{}) bool { + if isJSONNumber(left) && isJSONNumber(right) { + cmp, err := types.CompareJSON(context.Background(), left, right) + return err == nil && cmp == 0 + } + lArr, lIsArr := left.([]interface{}) rArr, rIsArr := right.([]interface{}) if lIsArr && rIsArr { @@ -121,11 +129,24 @@ func jsonEquals(left, right interface{}) bool { return left == right } +// isJSONNumber reports whether value uses a supported JSON numeric representation. +func isJSONNumber(value interface{}) bool { + switch value.(type) { + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64, + float32, float64, *apd.Decimal: + return true + default: + return false + } +} + +// overlaps reports whether two JSON values overlap according to MySQL semantics. func overlaps(left, right interface{}) bool { switch lVal := left.(type) { - case nil, bool, string, float64, int64, uint64: + case nil, bool, string, float64, int64, uint64, *apd.Decimal: switch rVal := right.(type) { - case nil, bool, string, float64, int64, uint64, map[string]interface{}: + case nil, bool, string, float64, int64, uint64, *apd.Decimal, map[string]interface{}: return jsonEquals(left, right) case []interface{}: // scalar must be in array @@ -137,7 +158,7 @@ func overlaps(left, right interface{}) bool { } case map[string]interface{}: switch rVal := right.(type) { - case nil, bool, string, float64, int64, uint64: + case nil, bool, string, float64, int64, uint64, *apd.Decimal: return overlaps(right, left) case map[string]interface{}: // objects must have at least one key-value pair in common @@ -159,7 +180,7 @@ func overlaps(left, right interface{}) bool { } case []interface{}: switch rVal := right.(type) { - case nil, bool, string, float64, int64, uint64: + case nil, bool, string, float64, int64, uint64, *apd.Decimal: return overlaps(right, left) case map[string]interface{}: return overlaps(right, left) diff --git a/sql/expression/function/json/jsontests/json_overlaps_test.go b/sql/expression/function/json/jsontests/json_overlaps_test.go index 1707dbb804..6dc99ac538 100644 --- a/sql/expression/function/json/jsontests/json_overlaps_test.go +++ b/sql/expression/function/json/jsontests/json_overlaps_test.go @@ -23,7 +23,9 @@ import ( "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/expression" "github.com/dolthub/go-mysql-server/sql/expression/function/json" + "github.com/dolthub/go-mysql-server/sql/types" ) func TestJSONOverlaps(t *testing.T) { @@ -253,3 +255,43 @@ func TestJSONOverlaps(t *testing.T) { }) } } + +// TestJSONOverlapsExactDecimals verifies overlap comparisons without decimal rounding. +func TestJSONOverlapsExactDecimals(t *testing.T) { + fields := []sql.Expression{ + expression.NewGetField(0, types.JSON, "left", false), + expression.NewGetField(1, types.JSON, "right", false), + } + fn, err := json.NewJSONOverlaps(sql.NewEmptyContext(), fields...) + require.NoError(t, err) + + tests := []struct { + name string + left string + right string + expected bool + }{ + {name: "equal scale", left: `9007199254740992.10`, right: `9007199254740992.1`, expected: true}, + {name: "adjacent", left: `9007199254740992.1`, right: `9007199254740992.2`, expected: false}, + {name: "nested object", left: `{"nested":{"n":1234567890.123456789}}`, right: `{"nested":{"n":1234567890.1234567890}}`, expected: true}, + {name: "nested array equal", left: `[9007199254740992.1]`, right: `[9007199254740992.10]`, expected: true}, + {name: "nested array adjacent", left: `[9007199254740992.1]`, right: `[9007199254740992.2]`, expected: false}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + left := preciseJSONDocument(t, test.left) + right := preciseJSONDocument(t, test.right) + actual, err := fn.Eval(sql.NewEmptyContext(), sql.Row{left, right}) + require.NoError(t, err) + require.Equal(t, test.expected, actual) + }) + } +} + +// preciseJSONDocument parses an exact-decimal JSON test value. +func preciseJSONDocument(t *testing.T, input string) types.JSONDocument { + t.Helper() + var value any + require.NoError(t, types.JsonUnmarshalPreserveNumberPrecision([]byte(input), &value)) + return types.JSONDocument{Val: value} +} diff --git a/sql/types/json.go b/sql/types/json.go index 3da45b55a7..e97af8089c 100644 --- a/sql/types/json.go +++ b/sql/types/json.go @@ -212,16 +212,16 @@ func DeepCopyJson(v interface{}) interface{} { return nil } - switch v.(type) { + switch v := v.(type) { case map[string]interface{}: - m := v.(map[string]interface{}) + m := v newMap := make(map[string]interface{}) for k, value := range m { newMap[k] = DeepCopyJson(value) } return newMap case []interface{}: - arr := v.([]interface{}) + arr := v newArray := make([]interface{}, len(arr)) for i, doc := range arr { newArray[i] = DeepCopyJson(doc) @@ -231,6 +231,10 @@ func DeepCopyJson(v interface{}) interface{} { int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: return v + case *apd.Decimal: + return new(apd.Decimal).Set(v) + case apd.Decimal: + return *new(apd.Decimal).Set(&v) default: return nil } @@ -247,6 +251,43 @@ func MustJSON(s string) JSONDocument { // JsonUnmarshal unmarshals JSON data. It picks the best representation // for each number to avoid losing precision whenever possible. func JsonUnmarshal(data []byte, v *interface{}) error { + if err := decodeJson(data, v); err != nil { + return err + } + *v = convertJsonNumbers(*v) + return nil +} + +// JsonUnmarshalPreserveNumberPrecision unmarshals JSON data while representing every +// JSON number as an arbitrary-precision decimal. Callers that require MySQL JSON +// number normalization should use JsonUnmarshal instead. +func JsonUnmarshalPreserveNumberPrecision(data []byte, v *interface{}) error { + if err := decodeJson(data, v); err != nil { + return err + } + converted, err := convertJsonNumbersToDecimals(*v) + if err != nil { + return err + } + *v = converted + return nil +} + +// JsonUnmarshalPreserveNumberTokens unmarshals JSON while retaining each number's +// original lexical representation as a json.Number. This is useful for textual +// JSON dialects whose accepted exponent range is wider than numeric types. +func JsonUnmarshalPreserveNumberTokens(data []byte, v *interface{}) error { + return decodeJson(data, v) +} + +// JsonNumbersToDecimals recursively converts retained json.Number tokens to +// arbitrary-precision decimals without reparsing the surrounding document. +func JsonNumbersToDecimals(v interface{}) (interface{}, error) { + return convertJsonNumbersToDecimals(v) +} + +// decodeJson decodes one complete JSON value while retaining json.Number tokens. +func decodeJson(data []byte, v *interface{}) error { dec := json.NewDecoder(bytes.NewReader(data)) dec.UseNumber() if err := dec.Decode(v); err != nil { @@ -262,17 +303,16 @@ func JsonUnmarshal(data []byte, v *interface{}) error { if err != io.EOF { return err } - *v = convertJsonNumbers(*v) return nil } // convertJsonNumbers recursively walks a parsed JSON value and converts json.Number values to -// int64, uint64, or float64, choosing the most precise representation. +// int64, uint64, or float64, choosing the most precise MySQL-compatible representation. func convertJsonNumbers(v interface{}) interface{} { switch val := v.(type) { case json.Number: s := val.String() - // If the number contains a decimal point or exponent, treat as float + // If the number contains a decimal point or exponent, treat as float. f, _ := val.Float64() if strings.ContainsAny(s, ".eE") { return f @@ -305,3 +345,70 @@ func convertJsonNumbers(v interface{}) interface{} { return v } } + +// convertJsonNumbersToDecimals recursively converts JSON number tokens to exact decimals. +func convertJsonNumbersToDecimals(v interface{}) (interface{}, error) { + switch val := v.(type) { + case json.Number: + return newJSONDecimal(val.String()) + case map[string]interface{}: + for key, inner := range val { + converted, err := convertJsonNumbersToDecimals(inner) + if err != nil { + return nil, err + } + val[key] = converted + } + return val, nil + case []interface{}: + for i, inner := range val { + converted, err := convertJsonNumbersToDecimals(inner) + if err != nil { + return nil, err + } + val[i] = converted + } + return val, nil + default: + return val, nil + } +} + +// newJSONDecimal parses a JSON number across the full exponent range supported by apd. +func newJSONDecimal(input string) (*apd.Decimal, error) { + if decimal, _, err := apd.NewFromString(input); err == nil { + return decimal, nil + } + + negative := strings.HasPrefix(input, "-") + unsigned := strings.TrimPrefix(input, "-") + mantissa, exponentText, hasExponent := strings.Cut(unsigned, "e") + if !hasExponent { + mantissa, exponentText, hasExponent = strings.Cut(unsigned, "E") + } + if !hasExponent { + return nil, fmt.Errorf("invalid JSON decimal %q", input) + } + exponent, err := strconv.ParseInt(exponentText, 10, 64) + if err != nil { + return nil, err + } + if dot := strings.IndexByte(mantissa, '.'); dot >= 0 { + scale := int64(len(mantissa) - dot - 1) + if exponent < int64(math.MinInt32)+scale || exponent > int64(math.MaxInt32)+scale { + return nil, fmt.Errorf("JSON decimal exponent out of range") + } + exponent -= scale + mantissa = mantissa[:dot] + mantissa[dot+1:] + } + if exponent < math.MinInt32 || exponent > math.MaxInt32 { + return nil, fmt.Errorf("JSON decimal exponent out of range") + } + var coefficient apd.BigInt + if _, ok := coefficient.SetString(mantissa, 10); !ok { + return nil, fmt.Errorf("invalid JSON decimal %q", input) + } + decimal := apd.NewWithBigInt(&coefficient, int32(exponent)) + decimal.Negative = negative + return decimal, nil +} diff --git a/sql/types/json_encode.go b/sql/types/json_encode.go index c998e3e581..e3ac2a4740 100644 --- a/sql/types/json_encode.go +++ b/sql/types/json_encode.go @@ -330,8 +330,22 @@ func writeMarshalledValue(writer io.Writer, val interface{}) error { writer.Write([]byte{'"'}) return nil case *apd.Decimal: + if val.IsZero() { + writer.Write([]byte{'0'}) + return nil + } writer.Write([]byte(val.Text('f'))) return nil + case apd.Decimal: + if val.IsZero() { + writer.Write([]byte{'0'}) + return nil + } + writer.Write([]byte(val.Text('f'))) + return nil + case json.Number: + writer.Write([]byte(val.String())) + return nil case json.Marshaler: bytes, err := val.MarshalJSON() if err != nil { diff --git a/sql/types/json_precision_test.go b/sql/types/json_precision_test.go new file mode 100644 index 0000000000..5bb09b4b64 --- /dev/null +++ b/sql/types/json_precision_test.go @@ -0,0 +1,197 @@ +// Copyright 2026 Dolthub, 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 types + +import ( + "encoding/json" + "math" + "testing" + + "github.com/cockroachdb/apd/v3" + "github.com/stretchr/testify/require" +) + +// TestJsonUnmarshalPreserveNumberTokensSupportsUnboundedExponent verifies lexical number retention. +func TestJsonUnmarshalPreserveNumberTokensSupportsUnboundedExponent(t *testing.T) { + input := []byte(`{"exact":123456789012345678901234567890.123456789,"exponent":1e3000000000}`) + var value interface{} + require.NoError(t, JsonUnmarshalPreserveNumberTokens(input, &value)) + + object := value.(map[string]interface{}) + require.Equal(t, json.Number("123456789012345678901234567890.123456789"), object["exact"]) + require.Equal(t, json.Number("1e3000000000"), object["exponent"]) + encoded, err := MarshallJsonValue(value) + require.NoError(t, err) + require.Equal(t, string(input), string(encoded)) +} + +// TestJsonUnmarshalPreserveNumberPrecision verifies exact fixed-point decoding. +func TestJsonUnmarshalPreserveNumberPrecision(t *testing.T) { + input := []byte(`{"value":123456789012345678901234567890.123456789}`) + var value interface{} + require.NoError(t, JsonUnmarshalPreserveNumberPrecision(input, &value)) + + decimal, ok := value.(map[string]interface{})["value"].(*apd.Decimal) + require.True(t, ok) + require.Equal(t, "123456789012345678901234567890.123456789", decimal.Text('f')) + + encoded, err := MarshallJsonValue(value) + require.NoError(t, err) + require.JSONEq(t, string(input), string(encoded)) +} + +// TestJsonPrecisionNestedRoundTripAndDeepCopy verifies nested precision and copy isolation. +func TestJsonPrecisionNestedRoundTripAndDeepCopy(t *testing.T) { + input := []byte(`{"array":[9007199254740992.1,{"value":-1234567890.123456789}],"ordinary":1.25}`) + var value interface{} + require.NoError(t, JsonUnmarshalPreserveNumberPrecision(input, &value)) + + object := value.(map[string]interface{}) + array := object["array"].([]interface{}) + require.IsType(t, &apd.Decimal{}, array[0]) + require.IsType(t, &apd.Decimal{}, array[1].(map[string]interface{})["value"]) + require.Equal(t, "1.25", object["ordinary"].(*apd.Decimal).String()) + + clone := DeepCopyJson(value).(map[string]interface{}) + clonedDecimal := clone["array"].([]interface{})[0].(*apd.Decimal) + originalDecimal := array[0].(*apd.Decimal) + require.NotSame(t, originalDecimal, clonedDecimal) + clonedDecimal.Neg(clonedDecimal) + require.Equal(t, "9007199254740992.1", originalDecimal.Text('f')) + + encoded, err := MarshallJsonValue(value) + require.NoError(t, err) + require.JSONEq(t, string(input), string(encoded)) +} + +// TestCompareJSONPreservedDecimals verifies exact ordering across JSON numeric representations. +func TestCompareJSONPreservedDecimals(t *testing.T) { + left := mustPreciseJSON(t, `9007199254740992.1`) + right := mustPreciseJSON(t, `9007199254740992.2`) + cmp, err := CompareJSON(t.Context(), left, right) + require.NoError(t, err) + require.Negative(t, cmp) + + equalScale := mustPreciseJSON(t, `9007199254740992.10`) + cmp, err = CompareJSON(t.Context(), left, equalScale) + require.NoError(t, err) + require.Zero(t, cmp) + + cmp, err = CompareJSON(t.Context(), mustPreciseJSON(t, `1.0`), int64(1)) + require.NoError(t, err) + require.Zero(t, cmp) + + cmp, err = CompareJSON(t.Context(), left, float64(9007199254740992)) + require.NoError(t, err) + require.Positive(t, cmp) +} + +// TestContainsJSONPreservedDecimals verifies exact containment across JSON numeric representations. +func TestContainsJSONPreservedDecimals(t *testing.T) { + target := mustPreciseJSON(t, `[9007199254740992.1, 1.0]`).Val + + contained, err := ContainsJSON(target, mustPreciseJSON(t, `9007199254740992.10`).Val) + require.NoError(t, err) + require.True(t, contained) + + contained, err = ContainsJSON(target, int64(1)) + require.NoError(t, err) + require.True(t, contained) + + contained, err = ContainsJSON(target, mustPreciseJSON(t, `9007199254740992.2`).Val) + require.NoError(t, err) + require.False(t, contained) +} + +// TestJsonUnmarshalRetainsMySQLNumberRepresentations verifies that the ordinary +// decoder continues to normalize JSON text through float64 where MySQL does. +func TestJsonUnmarshalRetainsMySQLNumberRepresentations(t *testing.T) { + var value interface{} + require.NoError(t, JsonUnmarshal([]byte(`1234567890.123456789`), &value)) + require.IsType(t, float64(0), value) + require.Equal(t, float64(1234567890.1234567), value) + + require.NoError(t, JsonUnmarshal([]byte(`9007199254740993`), &value)) + require.Equal(t, int64(9007199254740993), value) + + require.NoError(t, JsonUnmarshal([]byte(`1e100000`), &value)) + require.True(t, math.IsInf(value.(float64), 1)) +} + +// TestJsonUnmarshalPreserveNumberPrecisionHandlesExponent verifies exact exponent decoding. +func TestJsonUnmarshalPreserveNumberPrecisionHandlesExponent(t *testing.T) { + var value interface{} + require.NoError(t, JsonUnmarshalPreserveNumberPrecision([]byte(`1.234567890123456789e100`), &value)) + require.Equal(t, "1.234567890123456789E+100", value.(*apd.Decimal).String()) + + value = nil + require.NoError(t, JsonUnmarshalPreserveNumberPrecision([]byte(`1e131071`), &value)) + require.Equal(t, int32(131071), value.(*apd.Decimal).Exponent) + + value = nil + require.NoError(t, JsonUnmarshalPreserveNumberPrecision([]byte(`-1.25e+131070`), &value)) + require.True(t, value.(*apd.Decimal).Negative) + require.Equal(t, int32(131068), value.(*apd.Decimal).Exponent) + require.Equal(t, "125", value.(*apd.Decimal).Coeff.String()) + + value = nil + require.Error(t, JsonUnmarshalPreserveNumberPrecision([]byte(`1e3000000000`), &value)) +} + +// TestJsonUnmarshalKeepsExactlyRepresentableFractionAsFloat verifies the float fast path. +func TestJsonUnmarshalKeepsExactlyRepresentableFractionAsFloat(t *testing.T) { + var value interface{} + require.NoError(t, JsonUnmarshal([]byte(`1.25`), &value)) + require.Equal(t, 1.25, value) +} + +// mustPreciseJSON parses a test document without normalizing its numbers through float64. +func mustPreciseJSON(t *testing.T, input string) JSONDocument { + t.Helper() + var value interface{} + require.NoError(t, JsonUnmarshalPreserveNumberPrecision([]byte(input), &value)) + return JSONDocument{Val: value} +} + +// BenchmarkJSONNumberDecoding compares the numeric policies used by MySQL, storage, and PostgreSQL. +func BenchmarkJSONNumberDecoding(b *testing.B) { + inputs := map[string][]byte{ + "ordinary": []byte(`{"id":42,"ratio":1.25,"items":[1,2,3,4],"nested":{"enabled":true}}`), + "precise": []byte(`{"id":42,"ratio":12345678901234567890.123456789,"items":[1.1,2.2,3.3,4.4]}`), + } + decoders := map[string]func([]byte, *interface{}) error{ + "mysql_normalized": JsonUnmarshal, + "exact": JsonUnmarshalPreserveNumberPrecision, + } + for inputName, input := range inputs { + for decoderName, decoder := range decoders { + b.Run(inputName+"/"+decoderName, func(b *testing.B) { + b.ReportAllocs() + benchmarkJSONNumberDecoder(b, input, decoder) + }) + } + } +} + +// benchmarkJSONNumberDecoder repeatedly decodes one document with the selected numeric policy. +func benchmarkJSONNumberDecoder(b *testing.B, input []byte, decoder func([]byte, *interface{}) error) { + b.Helper() + for i := 0; i < b.N; i++ { + var value interface{} + if err := decoder(input, &value); err != nil { + b.Fatal(err) + } + } +} diff --git a/sql/types/json_value.go b/sql/types/json_value.go index 55a55380f2..92a5644eec 100644 --- a/sql/types/json_value.go +++ b/sql/types/json_value.go @@ -16,6 +16,7 @@ package types import ( "bytes" + "cmp" "context" "database/sql/driver" "encoding/json" @@ -64,7 +65,7 @@ func MarshallJsonValue(value interface{}) ([]byte, error) { encoder := json.NewEncoder(buffer) // Prevents special characters like <, >, or & from being escaped. encoder.SetEscapeHTML(false) - err := encoder.Encode(value) + err := encoder.Encode(jsonMarshalValue(value)) if err != nil { return nil, err } @@ -74,6 +75,32 @@ func MarshallJsonValue(value interface{}) ([]byte, error) { return out, err } +// jsonMarshalValue converts arbitrary-precision decimals to json.Number before using the +// standard JSON encoder. apd.Decimal implements encoding.TextMarshaler, which would otherwise +// encode a decimal as a quoted JSON string. +func jsonMarshalValue(value interface{}) interface{} { + switch value := value.(type) { + case *apd.Decimal: + return json.Number(value.String()) + case apd.Decimal: + return json.Number(value.String()) + case map[string]interface{}: + result := make(map[string]interface{}, len(value)) + for key, item := range value { + result[key] = jsonMarshalValue(item) + } + return result + case []interface{}: + result := make([]interface{}, len(value)) + for i, item := range value { + result[i] = jsonMarshalValue(item) + } + return result + default: + return value + } +} + // JSONBytes returns or generates a byte array for the JSON representation of the underlying sql.JSONWrapper func MarshallJson(ctx context.Context, jsonWrapper sql.JSONWrapper) ([]byte, error) { if bytes, ok := jsonWrapper.(JSONBytes); ok { @@ -550,7 +577,7 @@ func ContainsJSON(a, b interface{}) (bool, error) { return containsJSONBool(a, b) case string: return containsJSONString(a, b) - case float64, int64, uint64: + case float64, int64, uint64, *apd.Decimal, apd.Decimal: return containsJSONNumber(a, b) default: return false, sql.ErrInvalidType.New(a) @@ -752,8 +779,9 @@ func CompareJSON(ctx context.Context, a, b interface{}) (int, error) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: return compareJSONNumber(a, b) case *apd.Decimal: - af, _ := a.Float64() - return compareJSONNumber(af, b) + return compareJSONNumber(a, b) + case apd.Decimal: + return compareJSONNumber(a, b) case sql.JSONWrapper: if jw, ok := b.(sql.JSONWrapper); ok { b, err = jw.ToInterface(ctx) @@ -767,7 +795,7 @@ func CompareJSON(ctx context.Context, a, b interface{}) (int, error) { } return CompareJSON(ctx, aVal, b) default: - return 0, sql.ErrInvalidType.New(a) + return 0, sql.ErrInvalidType.New(fmt.Sprintf("%T (%v)", a, a)) } } @@ -899,16 +927,105 @@ func compareJSONNumber(a, b interface{}) (int, error) { // a is lower precedence return -1, nil case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: + switch a.(type) { + case *apd.Decimal, apd.Decimal: + return compareJSONNumbersExact(a, v) + } return compareNumbers(a, v), nil case *apd.Decimal: - f, _ := v.Float64() - return compareNumbers(a, f), nil + return compareJSONNumbersExact(a, v) + case apd.Decimal: + return compareJSONNumbersExact(a, v) default: // a is higher precedence return 1, nil } } +// compareJSONNumbersExact compares supported JSON numbers without losing precision. +func compareJSONNumbersExact(a, b interface{}) (int, error) { + aInfinity, aIsInfinity := jsonNumberInfinity(a) + bInfinity, bIsInfinity := jsonNumberInfinity(b) + if aIsInfinity || bIsInfinity { + switch { + case aIsInfinity && bIsInfinity: + return cmp.Compare(aInfinity, bInfinity), nil + case aIsInfinity: + return aInfinity, nil + default: + return -bInfinity, nil + } + } + aDecimal, err := jsonNumberDecimal(a) + if err != nil { + return 0, err + } + bDecimal, err := jsonNumberDecimal(b) + if err != nil { + return 0, err + } + return aDecimal.Cmp(bDecimal), nil +} + +// jsonNumberInfinity returns the sign when value is a floating-point infinity. +func jsonNumberInfinity(value interface{}) (int, bool) { + var floatValue float64 + switch value := value.(type) { + case float32: + floatValue = float64(value) + case float64: + floatValue = value + default: + return 0, false + } + if math.IsInf(floatValue, 1) { + return 1, true + } + if math.IsInf(floatValue, -1) { + return -1, true + } + return 0, false +} + +// jsonNumberDecimal converts a finite supported JSON number to an exact decimal. +func jsonNumberDecimal(value interface{}) (*apd.Decimal, error) { + var text string + switch value := value.(type) { + case *apd.Decimal: + return value, nil + case apd.Decimal: + return &value, nil + case int: + text = strconv.FormatInt(int64(value), 10) + case int8: + text = strconv.FormatInt(int64(value), 10) + case int16: + text = strconv.FormatInt(int64(value), 10) + case int32: + text = strconv.FormatInt(int64(value), 10) + case int64: + text = strconv.FormatInt(value, 10) + case uint: + text = strconv.FormatUint(uint64(value), 10) + case uint8: + text = strconv.FormatUint(uint64(value), 10) + case uint16: + text = strconv.FormatUint(uint64(value), 10) + case uint32: + text = strconv.FormatUint(uint64(value), 10) + case uint64: + text = strconv.FormatUint(value, 10) + case float32: + text = strconv.FormatFloat(float64(value), 'g', -1, 32) + case float64: + text = strconv.FormatFloat(value, 'g', -1, 64) + default: + return nil, fmt.Errorf("unexpected JSON number %T", value) + } + decimal, _, err := apd.NewFromString(text) + return decimal, err +} + func (doc JSONDocument) Insert(ctx context.Context, path string, val sql.JSONWrapper) (MutableJSON, bool, error) { path = strings.TrimSpace(path) return doc.unwrapAndExecute(ctx, path, val, INSERT)