Skip to content
Draft
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
6 changes: 6 additions & 0 deletions enginetest/memory_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand Down
22 changes: 22 additions & 0 deletions enginetest/queries/json_scripts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
29 changes: 25 additions & 4 deletions sql/expression/function/json/json_overlaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions sql/expression/function/json/jsontests/json_overlaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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}
}
119 changes: 113 additions & 6 deletions sql/types/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
14 changes: 14 additions & 0 deletions sql/types/json_encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading