diff --git a/go.mod b/go.mod index 48848ad716..16c431c468 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,10 @@ require ( github.com/PuerkitoBio/goquery v1.8.1 github.com/cockroachdb/apd/v3 v3.2.3 github.com/cockroachdb/errors v1.7.5 - github.com/dolthub/dolt/go v0.40.5-0.20260827015047-6c83efb8fd6f + github.com/dolthub/dolt/go v0.40.5-0.20260827200740-8b73116fa3a0 github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 - github.com/dolthub/go-mysql-server v0.20.1-0.20260826232105-2476e46ebc77 + github.com/dolthub/go-mysql-server v0.20.1-0.20260827195059-69dc4447f79d github.com/dolthub/pg_query_go/v6 v6.0.0-20251215122834-fb20be4254d1 github.com/dolthub/sqllogictest/go v0.0.0-20260624223518-788480b24166 github.com/dolthub/vitess v0.0.0-20260819175407-19559ab533b7 diff --git a/go.sum b/go.sum index 982be80276..72880a3b6c 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,12 @@ github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP4 github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk= github.com/dolthub/dolt/go v0.40.5-0.20260827015047-6c83efb8fd6f h1:OyTEzFYwj+AkWesMifuS3NKWfMgZaT14CGhQ7nZHTBI= github.com/dolthub/dolt/go v0.40.5-0.20260827015047-6c83efb8fd6f/go.mod h1:LAIaD9W6cbyLdCc+B8GhtWqof4jg7Os9IZoGp3OXQkw= +github.com/dolthub/dolt/go v0.40.5-0.20260827194013-098bf0dd73ea h1:IqOekjewTKVDDGaG+YDczwp3NnlRrbOwQAlPzQB0hIo= +github.com/dolthub/dolt/go v0.40.5-0.20260827194013-098bf0dd73ea/go.mod h1:yGOSyFRHNRW3UtsLqzZUbfjdPGXBwLMg12P1PU/QHGo= +github.com/dolthub/dolt/go v0.40.5-0.20260827195913-9e9dfcea77de h1:SCsNUAmrrIONge0Skag9lIZrikj/1GX3lMpqwi8LgAI= +github.com/dolthub/dolt/go v0.40.5-0.20260827195913-9e9dfcea77de/go.mod h1:ex4vSajZXQXDdHwPY0kTHW09d1jmz+EiH0WqlSjMI/8= +github.com/dolthub/dolt/go v0.40.5-0.20260827200740-8b73116fa3a0 h1:ch0tcFOh2VOxwXpHyPkernZSd7GGpOBQolzulEY2wqM= +github.com/dolthub/dolt/go v0.40.5-0.20260827200740-8b73116fa3a0/go.mod h1:gz29UDKvKWw5QIpSPEB2QLZ9Ed9ToEOodllQiUqbSyM= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4 h1:0mg9QEFdkkBwJMxvz1tCjHYmfG2iIC6aShj1InDq9/M= github.com/dolthub/eventsapi_schema v0.0.0-20260715220557-d9b4a1c6b4d4/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM= github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww= @@ -258,6 +264,10 @@ github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83 h1:FEMjCGEroD github.com/dolthub/go-icu-regex v0.0.0-20260610153742-72563bc7ca83/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE= github.com/dolthub/go-mysql-server v0.20.1-0.20260826232105-2476e46ebc77 h1:VW2z70lyE+vvjNa20yAkkoLm13h9cJTa5LxlcMVnMOw= github.com/dolthub/go-mysql-server v0.20.1-0.20260826232105-2476e46ebc77/go.mod h1:7Z71DCeZPBuG92QTM7KvQbpWCeNGZEL7chOHuSD4TOo= +github.com/dolthub/go-mysql-server v0.20.1-0.20260827194013-baefbed57445 h1:k3Z3cjLp1+pC0A+MOAb3HR97JVYRIYaWro3vafqXPDk= +github.com/dolthub/go-mysql-server v0.20.1-0.20260827194013-baefbed57445/go.mod h1:7Z71DCeZPBuG92QTM7KvQbpWCeNGZEL7chOHuSD4TOo= +github.com/dolthub/go-mysql-server v0.20.1-0.20260827195059-69dc4447f79d h1:mJRLM6HPGku+Pj8qZe7O05RJbXJLKoxsB99PeqfU29M= +github.com/dolthub/go-mysql-server v0.20.1-0.20260827195059-69dc4447f79d/go.mod h1:7Z71DCeZPBuG92QTM7KvQbpWCeNGZEL7chOHuSD4TOo= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63 h1:OAsXLAPL4du6tfbBgK0xXHZkOlos63RdKYS3Sgw/dfI= github.com/dolthub/gozstd v0.0.0-20240423170813-23a2903bca63/go.mod h1:lV7lUeuDhH5thVGDCKXbatwKy2KW80L4rMT46n+Y2/Q= github.com/dolthub/ishell v0.0.0-20260414231531-5f031e3e9037 h1:oIW9HwuWrhxv+4HZxA+QQSKHLqWFyXZ2FmNjUYwkdiM= diff --git a/integration-tests/go-sql-server-driver/large_values_test.go b/integration-tests/go-sql-server-driver/large_values_test.go index 9fcbe25221..3407e1fdd2 100644 --- a/integration-tests/go-sql-server-driver/large_values_test.go +++ b/integration-tests/go-sql-server-driver/large_values_test.go @@ -19,6 +19,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -26,6 +27,41 @@ import ( driver "github.com/dolthub/doltgresql/integration-tests/go-sql-server-driver/driver" ) +// TestJSONBPrecisionAfterRestart verifies exact JSONB numerics through mutation, indexed storage, and server restart. +func TestJSONBPrecisionAfterRestart(t *testing.T) { + server := setupTestServer(t, "jsonb_precision_restart") + db, err := server.DB(driver.Connection{}) + require.NoError(t, err) + + _, err = db.Exec(`CREATE TABLE precision_docs (id int PRIMARY KEY, doc jsonb)`) + require.NoError(t, err) + _, err = db.Exec(`INSERT INTO precision_docs VALUES (1, '{"value":123456789012345678901234567890.123456789}')`) + require.NoError(t, err) + _, err = db.Exec(`UPDATE precision_docs SET doc = jsonb_set(doc, '{other}', 'true'::jsonb) WHERE id = 1`) + require.NoError(t, err) + _, err = db.Exec(`SELECT dolt_commit('-Am', 'persist precise jsonb')`) + require.NoError(t, err) + require.NoError(t, db.Close()) + + require.NoError(t, server.Restart(nil, nil)) + db, err = server.DB(driver.Connection{}) + require.NoError(t, err) + defer db.Close() + require.Eventually(t, func() bool { return db.Ping() == nil }, 10*time.Second, 100*time.Millisecond) + + var extracted, castValue string + var equal, ordered bool + err = db.QueryRow(`SELECT doc ->> 'value', (doc -> 'value')::numeric::text, + doc -> 'value' = '123456789012345678901234567890.123456789'::jsonb, + doc -> 'value' < '123456789012345678901234567890.123456790'::jsonb + FROM precision_docs WHERE id = 1`).Scan(&extracted, &castValue, &equal, &ordered) + require.NoError(t, err) + require.Equal(t, "123456789012345678901234567890.123456789", extracted) + require.Equal(t, extracted, castValue) + require.True(t, equal) + require.True(t, ordered) +} + // setupTestServer creates a standard single-server test environment. The repo // name is also used as the database name. Cleanup is registered with t.Cleanup. func setupTestServer(t *testing.T, repoName string) *driver.SqlServer { diff --git a/server/cast/jsonb.go b/server/cast/jsonb.go index b173711471..2bbea46cf6 100644 --- a/server/cast/jsonb.go +++ b/server/cast/jsonb.go @@ -38,20 +38,20 @@ func initJsonB(builtInCasts map[id.Cast]casts.Cast) { func jsonbGetInterface(ctx *sql.Context, val any) (any, error) { switch v := val.(type) { case sql.JSONWrapper: - return v.ToInterface(ctx) + return pgtypes.JSONBWrapperToInterface(ctx, v) case sql.StringWrapper: - var result any s, err := v.Unwrap(ctx) if err != nil { return nil, err } - if err := json.Unmarshal([]byte(s), &result); err != nil { + result, err := pgtypes.DecodeJSONBValue([]byte(s)) + if err != nil { return nil, errors.Errorf("invalid JSON: %v", err) } return result, nil case string: - var result any - if err := json.Unmarshal([]byte(v), &result); err != nil { + result, err := pgtypes.DecodeJSONBValue([]byte(v)) + if err != nil { return nil, errors.Errorf("invalid JSON: %v", err) } return result, nil diff --git a/server/connection_handler.go b/server/connection_handler.go index 8128eb92eb..07b5cb43fa 100644 --- a/server/connection_handler.go +++ b/server/connection_handler.go @@ -1755,7 +1755,7 @@ func castSQLError(err error) *pgconn.PgError { code = pgcode.DivisionByZero case sql.ErrValueOutOfRange.Is(err), pgtypes.ErrValueIsOutOfRangeForType.Is(err), pgtypes.ErrOutOfRange.Is(err), pgtypes.ErrInputOutOfRange.Is(err), - errors.Is(err, pgtypes.ErrCastOutOfRange): + pgtypes.ErrJSONNumberOutOfRange.Is(err), errors.Is(err, pgtypes.ErrCastOutOfRange): code = pgcode.NumericValueOutOfRange case pgtypes.ErrInvalidSyntaxForType.Is(err), sql.ErrInvalidValue.Is(err): code = pgcode.InvalidTextRepresentation diff --git a/server/functions/array_to_json.go b/server/functions/array_to_json.go index 92e399c558..5cce0e6648 100644 --- a/server/functions/array_to_json.go +++ b/server/functions/array_to_json.go @@ -100,7 +100,11 @@ func valueToJsonRaw(ctx *sql.Context, elemType *pgtypes.DoltgresType, val any) ( if val == nil { return json.RawMessage("null"), nil } - if v, ok := val.(pgtypes.JsonDocument); ok { + switch v := val.(type) { + case types.JSONBytes: + bytes, err := v.GetBytes(ctx) + return json.RawMessage(bytes), err + case pgtypes.JsonDocument: sb := strings.Builder{} pgtypes.JsonValueFormatter(&sb, v.Value) return json.RawMessage(sb.String()), nil diff --git a/server/functions/binary/concatenate.go b/server/functions/binary/concatenate.go index 279a3315e9..5225fc234c 100644 --- a/server/functions/binary/concatenate.go +++ b/server/functions/binary/concatenate.go @@ -156,11 +156,11 @@ func jsonb_concat_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1Int if !ok1 || !ok2 { return nil, fmt.Errorf("jsonb_concat: unexpected types %T, %T", val1Interface, val2Interface) } - v1, err := wrapper1.ToInterface(ctx) + v1, err := pgtypes.JSONBWrapperToInterface(ctx, wrapper1) if err != nil { return nil, err } - v2, err := wrapper2.ToInterface(ctx) + v2, err := pgtypes.JSONBWrapperToInterface(ctx, wrapper2) if err != nil { return nil, err } diff --git a/server/functions/binary/json.go b/server/functions/binary/json.go index d2ddf0ce3a..cd34b59e36 100644 --- a/server/functions/binary/json.go +++ b/server/functions/binary/json.go @@ -123,8 +123,14 @@ func toJSONWrapper(ctx *sql.Context, val any) (sql.JSONWrapper, error) { // jsonWrapperElementToText converts a JSON element (sql.JSONWrapper) to its text representation. // For string values, it returns the raw string without quotes. For other types, it returns the JSON representation. -func jsonWrapperElementToText(ctx *sql.Context, wrapper sql.JSONWrapper) (string, error) { - v, err := wrapper.ToInterface(ctx) +func jsonWrapperElementToText(ctx *sql.Context, wrapper sql.JSONWrapper, jsonb bool) (string, error) { + var v any + var err error + if jsonb { + v, err = pgtypes.JSONBWrapperToInterface(ctx, wrapper) + } else { + v, err = pgtypes.JSONWrapperToInterface(ctx, wrapper) + } if err != nil { return "", err } @@ -172,7 +178,7 @@ var jsonb_array_element = framework.Function2{ Callable: jsonb_array_element_callable, } -func jsonb_array_element_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { +func jsonb_array_element_callable(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { wrapper, ok := val1.(sql.JSONWrapper) if !ok { return nil, nil @@ -204,7 +210,13 @@ func jsonb_array_element_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, // Materialized fallback: covers wrappers that don't implement // ComparableJSON (e.g. literal jsonb values) and the negative-index // case on ComparableJSON arrays. - v, err := wrapper.ToInterface(ctx) + var v any + var err error + if dt[1] != nil { + v, err = pgtypes.JSONBWrapperToInterface(ctx, wrapper) + } else { + v, err = pgtypes.JSONWrapperToInterface(ctx, wrapper) + } if err != nil { return nil, err } @@ -254,7 +266,7 @@ var jsonb_object_field = framework.Function2{ Return: pgtypes.JsonB, Parameters: [2]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.Text}, Strict: true, - Callable: func(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { + Callable: func(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { wrapper, ok := val1.(sql.JSONWrapper) if !ok { return nil, nil @@ -282,7 +294,12 @@ var jsonb_object_field = framework.Function2{ // ComparableJSON (e.g. literal jsonb values), where the embedded // jsonpath library has trouble with edge cases like keys that // contain escaped quotes, or array operands with text paths. - v, err := wrapper.ToInterface(ctx) + var v any + if dt[1] != nil { + v, err = pgtypes.JSONBWrapperToInterface(ctx, wrapper) + } else { + v, err = pgtypes.JSONWrapperToInterface(ctx, wrapper) + } if err != nil { return nil, err } @@ -333,7 +350,7 @@ var jsonb_array_element_text = framework.Function2{ if !ok { return nil, nil } - return jsonWrapperElementToText(ctx, wrapper) + return jsonWrapperElementToText(ctx, wrapper, dt[1] != nil) }, } @@ -372,7 +389,7 @@ var jsonb_object_field_text = framework.Function2{ if !ok { return nil, nil } - return jsonWrapperElementToText(ctx, wrapper) + return jsonWrapperElementToText(ctx, wrapper, dt[1] != nil) }, } @@ -412,7 +429,7 @@ var jsonb_extract_path = framework.Function2{ Callable: jsonb_extract_path_callable, } -func jsonb_extract_path_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { +func jsonb_extract_path_callable(ctx *sql.Context, dt [3]*pgtypes.DoltgresType, val1 any, val2 any) (any, error) { cur, ok := val1.(sql.JSONWrapper) if !ok { return nil, nil @@ -442,7 +459,7 @@ func jsonb_extract_path_callable(ctx *sql.Context, _ [3]*pgtypes.DoltgresType, v if !ok { return nil, nil } - next, err := extractOneJsonPathStep(ctx, cur, textPath) + next, err := extractOneJsonPathStep(ctx, cur, textPath, dt[1] != nil) if err != nil { return nil, err } @@ -495,7 +512,7 @@ func extractJsonPathBySingleLookup(ctx *sql.Context, doc sql.JSONWrapper, paths if result != nil { // A non-nil result could still be semantically NULL - i, err := result.ToInterface(ctx) + i, err := pgtypes.JSONWrapperToInterface(ctx, result) if err != nil { return nil, false, err } @@ -514,7 +531,7 @@ func extractJsonPathBySingleLookup(ctx *sql.Context, doc sql.JSONWrapper, paths // object and as an integer index when the wrapper is an array. Returns nil if // the step cannot be resolved (missing key, out-of-range index, scalar // wrapper, or non-integer text on an array). -func extractOneJsonPathStep(ctx *sql.Context, cur sql.JSONWrapper, textPath string) (sql.JSONWrapper, error) { +func extractOneJsonPathStep(ctx *sql.Context, cur sql.JSONWrapper, textPath string, jsonb bool) (sql.JSONWrapper, error) { // Fast path: use the ComparableJSON.JsonType / SearchableJSON.Lookup // interfaces to avoid materializing the entire document. if comparable, ok := cur.(types.ComparableJSON); ok { @@ -541,7 +558,13 @@ func extractOneJsonPathStep(ctx *sql.Context, cur sql.JSONWrapper, textPath stri } // Materialized fallback for wrappers that don't implement ComparableJSON, // and for negative array indices on ones that do. - v, err := cur.ToInterface(ctx) + var v any + var err error + if jsonb { + v, err = pgtypes.JSONBWrapperToInterface(ctx, cur) + } else { + v, err = pgtypes.JSONWrapperToInterface(ctx, cur) + } if err != nil { return nil, err } @@ -607,7 +630,7 @@ func jsonb_extract_path_text_callable(ctx *sql.Context, dt [3]*pgtypes.DoltgresT if !ok { return nil, nil } - return jsonWrapperElementToText(ctx, wrapper) + return jsonWrapperElementToText(ctx, wrapper, dt[1] != nil) } // jsonb_contains represents the PostgreSQL function of the same name, taking the same parameters. @@ -658,7 +681,7 @@ var jsonb_exists = framework.Function2{ } return found != nil, nil } - value, err := wrapper.ToInterface(ctx) + value, err := pgtypes.JSONBWrapperToInterface(ctx, wrapper) if err != nil { return nil, err } @@ -731,7 +754,7 @@ var jsonb_exists_any = framework.Function2{ } return false, nil } - value, err := wrapper.ToInterface(ctx) + value, err := pgtypes.JSONBWrapperToInterface(ctx, wrapper) if err != nil { return nil, err } @@ -801,7 +824,7 @@ var jsonb_exists_all = framework.Function2{ } return true, nil } - value, err := wrapper.ToInterface(ctx) + value, err := pgtypes.JSONBWrapperToInterface(ctx, wrapper) if err != nil { return nil, err } diff --git a/server/functions/init.go b/server/functions/init.go index e3b3fbfaf0..eed13a8b73 100644 --- a/server/functions/init.go +++ b/server/functions/init.go @@ -132,6 +132,7 @@ func Init() { initJsonObjectKeys() initJsonStripNulls() initJsonTypeof() + initJsonbSet() initLcm() initLeft() initLength() diff --git a/server/functions/json.go b/server/functions/json.go index 608ae38a70..c7bbd7d697 100644 --- a/server/functions/json.go +++ b/server/functions/json.go @@ -15,22 +15,64 @@ package functions import ( + "bytes" + "context" + "encoding/json" "fmt" - "unsafe" "github.com/cockroachdb/errors" "github.com/dolthub/go-mysql-server/sql" "github.com/dolthub/go-mysql-server/sql/types" - "github.com/goccy/go-json" "github.com/dolthub/doltgresql/server/functions/framework" pgtypes "github.com/dolthub/doltgresql/server/types" "github.com/dolthub/doltgresql/utils" ) +// preciseJSONDocument retains the original bytes for storage while exposing a parsed value +// whose numbers have not been converted through float64. +type preciseJSONDocument struct { + bytes []byte + value any + jsonb bool +} + +// Clone returns an independent precision-preserving JSON document. +func (d *preciseJSONDocument) Clone(context.Context) sql.JSONWrapper { + bytes := append([]byte(nil), d.bytes...) + var value any + var err error + if d.jsonb { + value, err = pgtypes.DecodeJSONBValue(bytes) + } else { + value, err = pgtypes.DecodeJSONValue(bytes) + } + if err != nil { + // The bytes were validated when this document was created. + return &preciseJSONDocument{bytes: bytes, value: d.value, jsonb: d.jsonb} + } + return &preciseJSONDocument{bytes: bytes, value: value, jsonb: d.jsonb} +} + +// ToInterface returns the parsed precision-preserving JSON value. +func (d *preciseJSONDocument) ToInterface(context.Context) (any, error) { + return d.value, nil +} + +// GetBytes returns the validated JSON bytes retained by the document. +func (d *preciseJSONDocument) GetBytes(context.Context) ([]byte, error) { + return d.bytes, nil +} + // jsonWrapperToFormattedString converts a sql.JSONWrapper to a formatted JSON string with spaces (JSONB format). -func jsonWrapperToFormattedString(ctx *sql.Context, val sql.JSONWrapper) (string, error) { - v, err := val.ToInterface(ctx) +func jsonWrapperToFormattedString(ctx *sql.Context, val sql.JSONWrapper, jsonb bool) (string, error) { + var v any + var err error + if jsonb { + v, err = pgtypes.JSONBWrapperToInterface(ctx, val) + } else { + v, err = pgtypes.JSONWrapperToInterface(ctx, val) + } if err != nil { return "", err } @@ -84,19 +126,39 @@ var json_in = framework.Function1{ } func json_in_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + return decodeJSONInput(ctx, val, false) +} + +// decodeJSONInput validates and decodes PostgreSQL JSON or JSONB input. +func decodeJSONInput(ctx *sql.Context, val any, jsonb bool) (any, error) { input, err := framework.UnwrapString(ctx, val) if err != nil { return nil, err } var jsonVal any - err = json.Unmarshal(unsafe.Slice(unsafe.StringData(input), len(input)), &jsonVal) + if jsonb { + jsonVal, err = pgtypes.DecodeJSONBValue([]byte(input)) + } else { + jsonVal, err = pgtypes.DecodeJSONValue([]byte(input)) + } if err != nil { + if pgtypes.ErrJSONNumberOutOfRange.Is(err) { + return nil, err + } if len(input) > 10 { input = input[:10] + "..." } return nil, pgtypes.ErrInvalidSyntaxForType.New("json", input) } - return types.JSONDocument{Val: jsonVal}, nil + formatted, err := types.JSONDocument{Val: jsonVal}.JSONString() + if err != nil { + return nil, err + } + var compact bytes.Buffer + if err = json.Compact(&compact, []byte(formatted)); err != nil { + return nil, err + } + return &preciseJSONDocument{bytes: compact.Bytes(), value: jsonVal, jsonb: jsonb}, nil } // json_out represents the PostgreSQL function of json type IO output. @@ -129,7 +191,7 @@ func json_out_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (a return string(bytes), err case sql.JSONWrapper: // JSON type is stored as binary JSON (same as JSONB), so output is normalized with spaces - return jsonWrapperToFormattedString(ctx, v) + return jsonWrapperToFormattedString(ctx, v, false) default: return nil, fmt.Errorf("unexpected type for json_out: %T", val) } @@ -164,9 +226,9 @@ func jsonb_out_callable(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) ( if doc == nil { return nil, nil } - return jsonWrapperToFormattedString(ctx, doc.(sql.JSONWrapper)) + return jsonWrapperToFormattedString(ctx, doc.(sql.JSONWrapper), true) case sql.JSONWrapper: - return jsonWrapperToFormattedString(ctx, v) + return jsonWrapperToFormattedString(ctx, v, true) default: return nil, fmt.Errorf("unexpected type for jsonb_out: %T", val) } @@ -214,7 +276,7 @@ var json_send = framework.Function1{ jsonStr = v case sql.JSONWrapper: var err error - jsonStr, err = jsonWrapperToFormattedString(ctx, v) + jsonStr, err = jsonWrapperToFormattedString(ctx, v, false) if err != nil { return nil, err } diff --git a/server/functions/jsonb.go b/server/functions/jsonb.go index 0813c76b65..800ab9674c 100644 --- a/server/functions/jsonb.go +++ b/server/functions/jsonb.go @@ -43,7 +43,9 @@ var jsonb_in = framework.Function1{ Return: pgtypes.JsonB, Parameters: [1]*pgtypes.DoltgresType{pgtypes.Cstring}, Strict: true, - Callable: json_in_callable, + Callable: func(ctx *sql.Context, _ [2]*pgtypes.DoltgresType, val any) (any, error) { + return decodeJSONInput(ctx, val, true) + }, } // jsonb_out represents the PostgreSQL function of jsonb type IO output. diff --git a/server/functions/jsonb_set.go b/server/functions/jsonb_set.go new file mode 100644 index 0000000000..c078f46788 --- /dev/null +++ b/server/functions/jsonb_set.go @@ -0,0 +1,214 @@ +// 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 functions + +import ( + "fmt" + "strconv" + "strings" + + "github.com/dolthub/go-mysql-server/sql" + "github.com/dolthub/go-mysql-server/sql/types" + + "github.com/dolthub/doltgresql/postgres/parser/pgcode" + "github.com/dolthub/doltgresql/postgres/parser/pgerror" + "github.com/dolthub/doltgresql/server/functions/framework" + pgtypes "github.com/dolthub/doltgresql/server/types" +) + +// initJsonbSet registers the jsonb_set overloads. PostgreSQL defines the fourth +// argument with a default of true; built-in defaults are represented here by a +// three-argument overload that delegates to the same implementation. +func initJsonbSet() { + framework.RegisterFunction(jsonb_set_jsonb_text_array_jsonb) + framework.RegisterFunction(jsonb_set_jsonb_text_array_jsonb_bool) +} + +var jsonb_set_jsonb_text_array_jsonb = framework.Function3{ + Name: "jsonb_set", + Return: pgtypes.JsonB, + Parameters: [3]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray, pgtypes.JsonB}, + Strict: true, + Callable: func(ctx *sql.Context, _ [4]*pgtypes.DoltgresType, target, path, newValue any) (any, error) { + return jsonbSet(ctx, target, path, newValue, true) + }, +} + +var jsonb_set_jsonb_text_array_jsonb_bool = framework.Function4{ + Name: "jsonb_set", + Return: pgtypes.JsonB, + Parameters: [4]*pgtypes.DoltgresType{pgtypes.JsonB, pgtypes.TextArray, pgtypes.JsonB, pgtypes.Bool}, + Strict: true, + Callable: func(ctx *sql.Context, _ [5]*pgtypes.DoltgresType, target, path, newValue, createIfMissing any) (any, error) { + return jsonbSet(ctx, target, path, newValue, createIfMissing.(bool)) + }, +} + +// jsonbSet replaces or creates the value at a PostgreSQL JSONB path. +func jsonbSet(ctx *sql.Context, target, path, newValue any, createIfMissing bool) (any, error) { + targetValue, err := jsonbValueToInterface(ctx, target) + if err != nil { + return nil, err + } + newInterface, err := jsonbValueToInterface(ctx, newValue) + if err != nil { + return nil, err + } + pathElements, err := jsonbSetPath(ctx, path) + if err != nil { + return nil, err + } + + // PostgreSQL rejects scalar targets even when the path is empty. + switch targetValue.(type) { + case map[string]any, []any: + default: + return nil, pgerror.WithCandidateCode(fmt.Errorf("cannot set path in scalar"), pgcode.InvalidParameterValue) + } + + if len(pathElements) == 0 { + return types.JSONDocument{Val: targetValue}, nil + } + result, _, err := jsonbSetAtPath(targetValue, pathElements, newInterface, createIfMissing, 0) + if err != nil { + return nil, err + } + return types.JSONDocument{Val: result}, nil +} + +// jsonbValueToInterface materializes a JSONB value without losing numeric precision. +func jsonbValueToInterface(ctx *sql.Context, value any) (any, error) { + unwrapped, err := sql.UnwrapAny(ctx, value) + if err != nil { + return nil, err + } + wrapper, ok := unwrapped.(sql.JSONWrapper) + if !ok { + return nil, fmt.Errorf("expected JSON wrapper, got %T", unwrapped) + } + return pgtypes.JSONBWrapperToInterface(ctx, wrapper) +} + +// jsonbSetPath unwraps and validates a JSONB path array. +func jsonbSetPath(ctx *sql.Context, value any) ([]string, error) { + unwrapped, err := sql.UnwrapAny(ctx, value) + if err != nil { + return nil, err + } + values, ok := unwrapped.([]any) + if !ok { + return nil, fmt.Errorf("expected text array, got %T", unwrapped) + } + path := make([]string, len(values)) + for i, value := range values { + if value == nil { + return nil, pgerror.WithCandidateCode( + fmt.Errorf("path element at position %d is null", i+1), + pgcode.NullValueNotAllowed, + ) + } + path[i], err = framework.UnwrapString(ctx, value) + if err != nil { + return nil, err + } + } + return path, nil +} + +// jsonbSetAtPath returns the updated value and whether the requested path was +// found or created. It copies only containers along the modified path, leaving +// the input JSON document untouched. +func jsonbSetAtPath(current any, path []string, newValue any, createIfMissing bool, pathOffset int) (any, bool, error) { + key := path[0] + last := len(path) == 1 + + switch current := current.(type) { + case map[string]any: + existing, found := current[key] + if !found { + if !last || !createIfMissing { + return current, false, nil + } + result := cloneJSONObject(current) + result[key] = newValue + return result, true, nil + } + + if last { + result := cloneJSONObject(current) + result[key] = newValue + return result, true, nil + } + updated, changed, err := jsonbSetAtPath(existing, path[1:], newValue, createIfMissing, pathOffset+1) + if err != nil || !changed { + return current, false, err + } + result := cloneJSONObject(current) + result[key] = updated + return result, true, nil + + case []any: + // PostgreSQL parses JSON array path indexes as signed 32-bit integers. + // Its integer parser accepts leading C-locale whitespace, but deliberately + // rejects trailing whitespace. Keep key unchanged for the error message. + indexText := strings.TrimLeft(key, " \t\n\r\f\v") + parsedIndex, err := strconv.ParseInt(indexText, 10, 32) + if err != nil { + return nil, false, pgerror.WithCandidateCode( + fmt.Errorf("path element at position %d is not an integer: %q", pathOffset+1, key), + pgcode.InvalidTextRepresentation, + ) + } + index := parsedIndex + if index < 0 { + index += int64(len(current)) + } + if index >= 0 && index < int64(len(current)) { + if last { + result := append([]any(nil), current...) + result[index] = newValue + return result, true, nil + } + updated, changed, err := jsonbSetAtPath(current[index], path[1:], newValue, createIfMissing, pathOffset+1) + if err != nil || !changed { + return current, false, err + } + result := append([]any(nil), current...) + result[index] = updated + return result, true, nil + } + if !last || !createIfMissing { + return current, false, nil + } + if parsedIndex < 0 { + return append([]any{newValue}, current...), true, nil + } + return append(append([]any(nil), current...), newValue), true, nil + + default: + // PostgreSQL does not create missing intermediate containers and leaves + // the document unchanged when traversal reaches a scalar. + return current, false, nil + } +} + +// cloneJSONObject returns a shallow copy for copy-on-write path updates. +func cloneJSONObject(value map[string]any) map[string]any { + result := make(map[string]any, len(value)+1) + for key, item := range value { + result[key] = item + } + return result +} diff --git a/server/types/json_document.go b/server/types/json_document.go index 9671af01b1..1cb51f402c 100644 --- a/server/types/json_document.go +++ b/server/types/json_document.go @@ -15,18 +15,29 @@ package types import ( - "bytes" + "context" "regexp" "sort" "strings" "github.com/cockroachdb/apd/v3" "github.com/cockroachdb/errors" + "github.com/dolthub/go-mysql-server/sql" + gmstypes "github.com/dolthub/go-mysql-server/sql/types" "github.com/goccy/go-json" + goerrors "gopkg.in/src-d/go-errors.v1" "github.com/dolthub/doltgresql/utils" ) +const ( + maxJSONNumericIntegerDigits = 131072 + maxJSONNumericScale = 16383 +) + +// ErrJSONNumberOutOfRange is returned when a JSON number exceeds PostgreSQL's numeric limits. +var ErrJSONNumberOutOfRange = goerrors.NewKind("value overflows numeric format") + // jsonDocumentStringUnicodeRegex is used on a JsonDocument's string to find all Unicode escape sequences that have an // additional backslash. var jsonDocumentStringUnicodeRegex = regexp.MustCompile(`\\\\u([0-9A-Fa-f]{4})`) @@ -230,12 +241,8 @@ func JsonValueFormatter(sb *strings.Builder, value JsonValue) { // UnmarshalToJsonDocument converts a JSON document byte slice into the actual JSON document. func UnmarshalToJsonDocument(val []byte) (JsonDocument, error) { - var decoded interface{} - decoder := json.NewDecoder(bytes.NewReader(val)) - // UseNumber causes JSON numbers to be decoded as json.Number (string-backed) instead of - // float64, which ensures we preserve values and precision. - decoder.UseNumber() - if err := decoder.Decode(&decoded); err != nil { + decoded, err := DecodeJSONBValue(val) + if err != nil { return JsonDocument{}, err } jsonValue, err := ConvertToJsonDocument(decoded) @@ -245,6 +252,84 @@ func UnmarshalToJsonDocument(val []byte) (JsonDocument, error) { return JsonDocument{Value: jsonValue}, nil } +// DecodeJSONValue parses textual PostgreSQL JSON while retaining number tokens exactly. +// PostgreSQL's json type validates syntax but does not impose jsonb's numeric bounds. +func DecodeJSONValue(val []byte) (any, error) { + var decoded any + if err := gmstypes.JsonUnmarshalPreserveNumberTokens(val, &decoded); err != nil { + return nil, err + } + return decoded, nil +} + +// DecodeJSONBValue parses PostgreSQL jsonb without losing numeric precision and +// enforces the numeric range accepted by PostgreSQL's jsonb input function. +func DecodeJSONBValue(val []byte) (any, error) { + decoded, err := DecodeJSONValue(val) + if err != nil { + return nil, err + } + decoded, err = gmstypes.JsonNumbersToDecimals(decoded) + if err != nil { + return nil, ErrJSONNumberOutOfRange.New() + } + if err := validateJSONNumberRange(decoded); err != nil { + return nil, err + } + return decoded, nil +} + +// validateJSONNumberRange enforces PostgreSQL's numeric bounds for JSONB numbers. +func validateJSONNumberRange(value any) error { + switch value := value.(type) { + case *apd.Decimal: + exponent := int64(value.Exponent) + if exponent < -maxJSONNumericScale { + return ErrJSONNumberOutOfRange.New() + } + if !value.IsZero() && value.NumDigits()+exponent > maxJSONNumericIntegerDigits { + return ErrJSONNumberOutOfRange.New() + } + case map[string]any: + for _, item := range value { + if err := validateJSONNumberRange(item); err != nil { + return err + } + } + case []any: + for _, item := range value { + if err := validateJSONNumberRange(item); err != nil { + return err + } + } + } + return nil +} + +// JSONWrapperToInterface returns a JSON wrapper's native value without normalizing +// exact numeric tokens through float64 when the wrapper exposes its stored bytes. +func JSONWrapperToInterface(ctx context.Context, wrapper sql.JSONWrapper) (any, error) { + return jsonWrapperToInterface(ctx, wrapper, DecodeJSONValue) +} + +// JSONBWrapperToInterface materializes byte-backed jsonb using exact, bounded +// PostgreSQL numeric semantics. +func JSONBWrapperToInterface(ctx context.Context, wrapper sql.JSONWrapper) (any, error) { + return jsonWrapperToInterface(ctx, wrapper, DecodeJSONBValue) +} + +// jsonWrapperToInterface decodes byte-backed wrappers with the requested numeric policy. +func jsonWrapperToInterface(ctx context.Context, wrapper sql.JSONWrapper, decode func([]byte) (any, error)) (any, error) { + if bytesValue, ok := wrapper.(gmstypes.JSONBytes); ok { + bytes, err := bytesValue.GetBytes(ctx) + if err != nil { + return nil, err + } + return decode(bytes) + } + return wrapper.ToInterface(ctx) +} + // ConvertToJsonDocument recursively constructs a valid JsonDocument based on the structures returned by the decoder. func ConvertToJsonDocument(val interface{}) (JsonValue, error) { var err error diff --git a/server/types/jsonb.go b/server/types/jsonb.go index a19660444e..d6734ec0d9 100644 --- a/server/types/jsonb.go +++ b/server/types/jsonb.go @@ -74,7 +74,7 @@ func serializeTypeJsonB(ctx *sql.Context, t *DoltgresType, val any) ([]byte, err var doc JsonDocument switch v := res.(type) { case sql.JSONWrapper: - j, err := v.ToInterface(ctx) + j, err := JSONBWrapperToInterface(ctx, v) if err != nil { return nil, err } diff --git a/testing/go/jsonb_set_test.go b/testing/go/jsonb_set_test.go new file mode 100644 index 0000000000..0f7d5a7a86 --- /dev/null +++ b/testing/go/jsonb_set_test.go @@ -0,0 +1,301 @@ +// 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 _go + +import ( + "testing" + + "github.com/dolthub/go-mysql-server/sql" +) + +// TestJsonbSet verifies PostgreSQL-compatible JSONB mutation and numeric behavior. +func TestJsonbSet(t *testing.T) { + RunScripts(t, []ScriptTest{ + { + Name: "json preserves exact numbers outside jsonb numeric range", + SetUpScript: []string{ + `CREATE TABLE json_number_text_test (id INT PRIMARY KEY, value JSON);`, + `INSERT INTO json_number_text_test VALUES (1, '{"n":123456789012345678901234567890.123456789}'), (2, '{"n":1e3000000000}');`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT ('123456789012345678901234567890.123456789'::json)::text, ('1e131072'::json)::text, ('1e3000000000'::json)::text;`, + Expected: []sql.Row{{`123456789012345678901234567890.123456789`, `1e131072`, `1e3000000000`}}, + }, + { + Query: `SELECT value ->> 'n' FROM json_number_text_test ORDER BY id;`, + Expected: []sql.Row{{`123456789012345678901234567890.123456789`}, {`1e3000000000`}}, + }, + }, + }, + { + Name: "jsonb_set objects and paths", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT '1e308'::jsonb IS NOT NULL, '1e-16383'::jsonb IS NOT NULL, '0e1000000000'::jsonb IS NOT NULL;`, + ExpectedRaw: [][][]byte{{{1}, {1}, {1}}}, + }, + { + Query: `SELECT '1e131072'::jsonb;`, + ExpectedErr: "value overflows numeric format", + ExpectedErrCode: "22003", + }, + { + Query: `SELECT '{"n":1e-16384}'::jsonb;`, + ExpectedErr: "value overflows numeric format", + ExpectedErrCode: "22003", + }, + { + Query: `SELECT jsonb_set('{"a":1,"b":2}'::jsonb, '{a}', '9'::jsonb);`, + Expected: []sql.Row{{`{"a": 9, "b": 2}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{b}', '2'::jsonb);`, + Expected: []sql.Row{{`{"a": 1, "b": 2}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":{"b":1}}'::jsonb, '{a,b}', '2'::jsonb);`, + Expected: []sql.Row{{`{"a": {"b": 2}}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{b}', '2'::jsonb, true);`, + Expected: []sql.Row{{`{"a": 1, "b": 2}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{b}', '2'::jsonb, false);`, + Expected: []sql.Row{{`{"a": 1}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{b,c}', '2'::jsonb, true);`, + Expected: []sql.Row{{`{"a": 1}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{a,b}', '2'::jsonb);`, + Expected: []sql.Row{{`{"a": 1}`}}, + }, + { + Query: `WITH input(doc) AS (VALUES ('{"a":{"b":1},"c":3}'::jsonb)) + SELECT doc, jsonb_set(doc, '{a,b}', '2'::jsonb), doc FROM input;`, + Expected: []sql.Row{{ + `{"a": {"b": 1}, "c": 3}`, + `{"a": {"b": 2}, "c": 3}`, + `{"a": {"b": 1}, "c": 3}`, + }}, + }, + }, + }, + { + Name: "jsonb_set arrays", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, '{1}', '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, '{-1}', '9'::jsonb);`, + Expected: []sql.Row{{`[0, 1, 9]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, '{9}', '9'::jsonb);`, + Expected: []sql.Row{{`[0, 1, 2, 9]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, '{-9}', '9'::jsonb);`, + Expected: []sql.Row{{`[9, 0, 1, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[{"a":1}]'::jsonb, '{0,a}', '2'::jsonb);`, + Expected: []sql.Row{{`[{"a": 2}]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, '{3}', '9'::jsonb, false);`, + Expected: []sql.Row{{`[0, 1, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[]'::jsonb, '{0}', '1'::jsonb);`, + Expected: []sql.Row{{`[1]`}}, + }, + { + Query: `SELECT jsonb_set('[]'::jsonb, '{-1}', '1'::jsonb);`, + Expected: []sql.Row{{`[1]`}}, + }, + }, + }, + { + Name: "jsonb_set array index whitespace", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[' 1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[E'\t1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[E'\n1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[E'\r1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[E'\f1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[chr(11) || '1'], '9'::jsonb);`, + Expected: []sql.Row{{`[0, 9, 2]`}}, + }, + { + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY['1 '], '9'::jsonb);`, + ExpectedErr: `path element at position 1 is not an integer: "1 "`, + }, + { + // The path starts with U+00A0 NO-BREAK SPACE, which is not C-locale whitespace. + Query: `SELECT jsonb_set('[0,1,2]'::jsonb, ARRAY[' 1'], '9'::jsonb);`, + ExpectedErr: "path element at position 1 is not an integer", + }, + }, + }, + { + Name: "jsonb_set empty, null, and scalar inputs", + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{}'::text[], '2'::jsonb);`, + Expected: []sql.Row{{`{"a": 1}`}}, + }, + { + Query: `SELECT jsonb_set('{}'::jsonb, '{a}', '1'::jsonb);`, + Expected: []sql.Row{{`{"a": 1}`}}, + }, + { + Query: `SELECT jsonb_set('{"a":1}'::jsonb, '{a}', 'null'::jsonb);`, + Expected: []sql.Row{{`{"a": null}`}}, + }, + { + Query: `SELECT jsonb_set(NULL::jsonb, '{a}', '1'::jsonb);`, + Expected: []sql.Row{{nil}}, + }, + { + Query: `SELECT jsonb_set('{}'::jsonb, NULL::text[], '1'::jsonb);`, + Expected: []sql.Row{{nil}}, + }, + { + Query: `SELECT jsonb_set('{}'::jsonb, '{a}', NULL::jsonb);`, + Expected: []sql.Row{{nil}}, + }, + { + Query: `SELECT jsonb_set('{}'::jsonb, '{a}', '1'::jsonb, NULL::boolean);`, + Expected: []sql.Row{{nil}}, + }, + { + Query: `SELECT jsonb_set('{}'::jsonb, ARRAY[NULL]::text[], '1'::jsonb);`, + ExpectedErr: "path element at position 1 is null", + }, + { + Query: `SELECT jsonb_set('1'::jsonb, '{a}', '2'::jsonb);`, + ExpectedErr: "cannot set path in scalar", + }, + { + Query: `SELECT jsonb_set('null'::jsonb, '{a}', '2'::jsonb);`, + ExpectedErr: "cannot set path in scalar", + }, + { + Query: `SELECT jsonb_set('1'::jsonb, '{}'::text[], '2'::jsonb);`, + ExpectedErr: "cannot set path in scalar", + }, + }, + }, + { + Name: "jsonb_set validation and canonical output", + SetUpScript: []string{ + `CREATE TABLE jsonb_precision_test (id INT PRIMARY KEY, value JSONB);`, + `INSERT INTO jsonb_precision_test VALUES (1, '{"n":0}');`, + `INSERT INTO jsonb_precision_test VALUES (2, '{"value":123456789012345678901234567890.123456789}');`, + `UPDATE jsonb_precision_test SET value = jsonb_set(value, '{other}', 'true'::jsonb) WHERE id = 2;`, + }, + Assertions: []ScriptTestAssertion{ + { + Query: `SELECT '123456789012345678901234567890'::jsonb, '1234567890.12345678901234567890'::jsonb;`, + ExpectedRaw: [][][]byte{{ + []byte(`123456789012345678901234567890`), + []byte(`1234567890.12345678901234567890`), + }}, + }, + { + Query: `SELECT jsonb_set(value, '{n}', '1234567890.12345678901234567890'::jsonb) FROM jsonb_precision_test WHERE id = 1;`, + ExpectedRaw: [][][]byte{{ + []byte(`{"n": 1234567890.12345678901234567890}`), + }}, + }, + { + Query: `SELECT value FROM jsonb_precision_test WHERE id = 2;`, + ExpectedRaw: [][][]byte{{ + []byte(`{"other": true, "value": 123456789012345678901234567890.123456789}`), + }}, + }, + { + Query: `SELECT value ->> 'value', value #>> '{value}', (value -> 'value')::numeric = 123456789012345678901234567890.123456789::numeric, value -> 'value' = '123456789012345678901234567890.123456789'::jsonb, value -> 'value' < '123456789012345678901234567890.123456790'::jsonb FROM jsonb_precision_test WHERE id = 2;`, + ExpectedRaw: [][][]byte{{ + []byte(`123456789012345678901234567890.123456789`), + []byte(`123456789012345678901234567890.123456789`), + {1}, + {1}, + {1}, + }}, + }, + { + Query: `SELECT value || '{"third": 123456789012345678901234567890.123456788}'::jsonb FROM jsonb_precision_test WHERE id = 2;`, + ExpectedRaw: [][][]byte{{ + []byte(`{"other": true, "third": 123456789012345678901234567890.123456788, "value": 123456789012345678901234567890.123456789}`), + }}, + }, + { + Query: `SELECT '1.0'::jsonb = '1'::jsonb, '9007199254740992.1'::jsonb = '9007199254740992.2'::jsonb, '9007199254740992.1'::jsonb < '9007199254740992.2'::jsonb;`, + ExpectedRaw: [][][]byte{{{1}, {0}, {1}}}, + }, + { + Query: `SELECT jsonb_set('{"n":0}'::jsonb, '{n}', '123456789012345678901234567890'::jsonb), jsonb_set('{"n":0}'::jsonb, '{n}', '1234567890.12345678901234567890'::jsonb);`, + ExpectedRaw: [][][]byte{{ + []byte(`{"n": 123456789012345678901234567890}`), + []byte(`{"n": 1234567890.12345678901234567890}`), + }}, + }, + { + Query: `SELECT jsonb_set('[]'::jsonb, '{x}', '1'::jsonb);`, + ExpectedErr: `path element at position 1 is not an integer: "x"`, + }, + { + Query: `SELECT jsonb_set('[0]'::jsonb, '{999999999999999999999999}', '1'::jsonb);`, + ExpectedErr: `path element at position 1 is not an integer: "999999999999999999999999"`, + }, + { + Query: `SELECT jsonb_set('[0]'::jsonb, '{2147483648}', '1'::jsonb);`, + ExpectedErr: `path element at position 1 is not an integer: "2147483648"`, + }, + { + Query: `SELECT jsonb_set('{"a.b":{"q\"uote":1}}'::jsonb, ARRAY['a.b','q"uote'], '"x\\y"'::jsonb);`, + Expected: []sql.Row{{`{"a.b": {"q\"uote": "x\\y"}}`}}, + }, + { + Query: `SELECT jsonb_set('{"b":0,"a":1,"a":2}'::jsonb, '{c}', '{"z":1,"z":2,"a":3}'::jsonb);`, + Expected: []sql.Row{{`{"a": 2, "b": 0, "c": {"a": 3, "z": 2}}`}}, + }, + }, + }, + }) +}