Files
dolthub--doltgresql/testing/go/json_function_test.go
T
2026-07-13 12:32:25 +08:00

964 lines
29 KiB
Go

// 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 (
"fmt"
"strings"
"testing"
"github.com/dolthub/go-mysql-server/sql"
)
// makeLargeJSONObject builds a JSONB object literal with the given number of
// keys named k_0000…k_NNNN, each mapped to a small nested object. With 100
// keys the serialized form is roughly 8 KB, which is comfortably above the
// 4 KB threshold that triggers out-of-band storage in Dolt's indexed JSON
// document representation.
func makeLargeJSONObject(numKeys int) string {
var b strings.Builder
b.WriteByte('{')
for i := 0; i < numKeys; i++ {
if i > 0 {
b.WriteByte(',')
}
fmt.Fprintf(&b,
`"k_%04d":{"name":"value_%04d","tags":["tag-a","tag-b","tag-c","tag-d","tag-e"],"n":%d}`,
i, i, i)
}
b.WriteByte('}')
return b.String()
}
// makeLargeJSONArray builds a JSONB array literal with the given number of
// element objects, each labeled row_0000…row_NNNN. With 80 elements the
// serialized form is roughly 5 KB.
func makeLargeJSONArray(numElems int) string {
var b strings.Builder
b.WriteByte('[')
for i := 0; i < numElems; i++ {
if i > 0 {
b.WriteByte(',')
}
fmt.Fprintf(&b,
`{"id":%d,"label":"row_%04d","payload":["a","b","c","d","e"]}`,
i, i)
}
b.WriteByte(']')
return b.String()
}
// makeLargeJSONObjectWithNumericKeys builds a JSONB object large enough to be
// stored as an indexed document, whose "nums" key maps to a nested object with
// numeric string keys. It exercises the extract-path fast path's fallback: a
// numeric path element is first guessed as an array index, which an object
// rejects, so resolution must fall back to treating it as an object key.
func makeLargeJSONObjectWithNumericKeys() string {
padding := makeLargeJSONObject(100)
// Splice a numeric-keyed sub-object onto the front of the padding object,
// dropping the padding's leading '{'.
return `{"nums":{"0":"zero","1":"one","2":"two"},` + padding[1:]
}
// TestJsonObjectField exercises the `->` operator with a text right-hand side
// against both jsonb and json values (jsonb_object_field / json_object_field),
// plus the `->>` text-returning variants. The optimization path uses
// types.LookupJSONValue against SearchableJSON wrappers, but the semantics
// must match for non-object inputs and special keys as well.
func TestJsonObjectField(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "jsonb_object_field returns object value",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":1,"b":"two"}'::jsonb -> 'a';`,
Expected: []sql.Row{{`1`}},
},
{
Query: `SELECT '{"a":1,"b":"two"}'::jsonb -> 'b';`,
Expected: []sql.Row{{`"two"`}},
},
{
Query: `SELECT '{"a":1,"b":"two"}'::jsonb -> null;`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT '{"nested":{"x":[1,2,3]}}'::jsonb -> 'nested';`,
Expected: []sql.Row{{`{"x": [1, 2, 3]}`}},
},
{
// Missing key returns SQL NULL.
Query: `SELECT '{"a":1}'::jsonb -> 'missing';`,
Expected: []sql.Row{{nil}},
},
{
// `->` with a text key on an array returns SQL NULL.
Query: `SELECT '[1,2,3]'::jsonb -> 'a';`,
Expected: []sql.Row{{nil}},
},
{
// `->` with a text key on a scalar returns SQL NULL.
Query: `SELECT '42'::jsonb -> 'a';`,
Expected: []sql.Row{{nil}},
},
{
// Key with a literal dot in it: the optimized lookup
// builds a quoted-key MySQL JSON path, so the dot must
// not be treated as a path separator.
Query: `SELECT '{"a.b":1, "a":{"b":2}}'::jsonb -> 'a.b';`,
Expected: []sql.Row{{`1`}},
},
{
// Key containing a literal double-quote, which must be
// escaped in the constructed MySQL JSON path.
Query: `SELECT '{"a\"b":7}'::jsonb -> 'a"b';`,
Expected: []sql.Row{{`7`}},
},
},
},
{
Name: "json_object_field returns object value",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":1,"b":"two"}'::json -> 'a';`,
Expected: []sql.Row{{`1`}},
},
{
Query: `SELECT '{"a":1,"b":"two"}'::json -> 'b';`,
Expected: []sql.Row{{`"two"`}},
},
{
Query: `SELECT '{"a":1}'::json -> 'missing';`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT '[1,2,3]'::json -> 'a';`,
Expected: []sql.Row{{nil}},
},
},
},
{
Name: "jsonb_object_field_text returns object value as text",
Assertions: []ScriptTestAssertion{
{
// `->>` on a string value returns the raw string (no
// surrounding quotes).
Query: `SELECT '{"a":1,"b":"two"}'::jsonb ->> 'b';`,
Expected: []sql.Row{{`two`}},
},
{
// Numeric value is rendered as its JSON text.
Query: `SELECT '{"a":42}'::jsonb ->> 'a';`,
Expected: []sql.Row{{`42`}},
},
{
// Nested object is rendered as the JSON object text.
Query: `SELECT '{"a":{"b":1}}'::jsonb ->> 'a';`,
Expected: []sql.Row{{`{"b": 1}`}},
},
{
Query: `SELECT '{"a":1}'::jsonb ->> 'missing';`,
Expected: []sql.Row{{nil}},
},
},
},
})
}
// TestJsonArrayElement exercises the `->` operator with an integer right-hand
// side (jsonb_array_element / json_array_element) and the `->>` text variant.
// The optimized path uses $[N] lookups; negative indices fall back to a
// materialized walk to resolve the absolute index.
func TestJsonArrayElement(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "jsonb_array_element returns array element",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '[10,20,30]'::jsonb -> 0;`,
Expected: []sql.Row{{`10`}},
},
{
Query: `SELECT '[10,20,30]'::jsonb -> 2;`,
Expected: []sql.Row{{`30`}},
},
{
// Out-of-range positive index returns SQL NULL.
Query: `SELECT '[10,20,30]'::jsonb -> 5;`,
Expected: []sql.Row{{nil}},
},
{
// Negative indices count from the end.
Query: `SELECT '[10,20,30]'::jsonb -> -1;`,
Expected: []sql.Row{{`30`}},
},
{
Query: `SELECT '[10,20,30]'::jsonb -> -3;`,
Expected: []sql.Row{{`10`}},
},
{
// Out-of-range negative index returns SQL NULL.
Query: `SELECT '[10,20,30]'::jsonb -> -5;`,
Expected: []sql.Row{{nil}},
},
{
// Indexing a non-array returns SQL NULL.
Query: `SELECT '{"a":1}'::jsonb -> 0;`,
Expected: []sql.Row{{nil}},
},
{
// Indexing a scalar returns SQL NULL.
Query: `SELECT '42'::jsonb -> 0;`,
Expected: []sql.Row{{nil}},
},
{
// Nested object element survives the lookup with full
// structure intact.
Query: `SELECT '[{"a":1},{"b":2}]'::jsonb -> 1;`,
Expected: []sql.Row{{`{"b": 2}`}},
},
},
},
{
Name: "json_array_element returns array element",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '[10,20,30]'::json -> 1;`,
Expected: []sql.Row{{`20`}},
},
{
Query: `SELECT '[10,20,30]'::json -> -1;`,
Expected: []sql.Row{{`30`}},
},
{
Query: `SELECT '[10,20,30]'::json -> 99;`,
Expected: []sql.Row{{nil}},
},
},
},
{
Name: "jsonb_array_element_text returns text representation",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '["alpha","beta"]'::jsonb ->> 0;`,
Expected: []sql.Row{{`alpha`}},
},
{
Query: `SELECT '[10,20,30]'::jsonb ->> -1;`,
Expected: []sql.Row{{`30`}},
},
{
Query: `SELECT '[10,20,30]'::jsonb ->> 99;`,
Expected: []sql.Row{{nil}},
},
},
},
})
}
// TestJsonExtractPath exercises the `#>` operator (jsonb_extract_path /
// json_extract_path) and the text-returning `#>>` variant. The path is a
// text array; each element selects a key on an object or an integer index
// on an array at the current location.
func TestJsonExtractPath(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "jsonb_extract_path follows mixed key/index paths",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":{"b":{"c":1}}}'::jsonb #> '{a,b,c}';`,
Expected: []sql.Row{{`1`}},
},
{
Query: `SELECT '{"a":[10,20,30]}'::jsonb #> '{a,1}';`,
Expected: []sql.Row{{`20`}},
},
{
Query: `SELECT '{"a":[10,20,30]}'::jsonb #> '{a,-1}';`,
Expected: []sql.Row{{`30`}},
},
{
Query: `SELECT '{"a":[10,20]}'::jsonb #> '{a,not-an-int}';`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT '{"a":{"b":1}}'::jsonb #> '{a,missing,c}';`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT '{"a":1}'::jsonb #> '{a,b}';`,
Expected: []sql.Row{{nil}},
},
},
},
{
Name: "jsonb_extract_path_text renders the leaf as text",
Assertions: []ScriptTestAssertion{
{
// String leaf returns the raw string.
Query: `SELECT '{"a":{"b":"hello"}}'::jsonb #>> '{a,b}';`,
Expected: []sql.Row{{`hello`}},
},
{
// Object leaf returns the JSON text of the object.
Query: `SELECT '{"a":{"b":{"c":1}}}'::jsonb #>> '{a,b}';`,
Expected: []sql.Row{{`{"c": 1}`}},
},
{
Query: `SELECT '{"a":[1,2,3]}'::jsonb #>> '{a,2}';`,
Expected: []sql.Row{{`3`}},
},
{
Query: `SELECT '{"a":1}'::jsonb #>> '{missing}';`,
Expected: []sql.Row{{nil}},
},
},
},
{
Name: "json_extract_path follows mixed key/index paths",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":{"b":[10,20]}}'::json #> '{a,b,0}';`,
Expected: []sql.Row{{`10`}},
},
{
Query: `SELECT '{"a":1}'::json #> '{missing}';`,
Expected: []sql.Row{{nil}},
},
},
},
{
Name: "jsonb_extract_path with multi-element text-array paths",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":{"b":42}}'::jsonb #> ARRAY['a','b'];`,
Expected: []sql.Row{{`42`}},
},
{
// A deeper path mixing object keys and an array index.
Query: `SELECT '{"a":{"b":{"c":[10,20]}}}'::jsonb #> ARRAY['a','b','c','1'];`,
Expected: []sql.Row{{`20`}},
},
{
Query: `SELECT '{"a":{"b":42}}'::jsonb #>> ARRAY['a','b'];`,
Expected: []sql.Row{{`42`}},
},
{
// The string 'NULL' is an ordinary key, distinct from a SQL
// NULL element: both the ARRAY['NULL'] form and the quoted
// '{"NULL"}' literal select the key named "NULL".
Query: `SELECT '{"NULL":7}'::jsonb #> ARRAY['NULL'];`,
Expected: []sql.Row{{`7`}},
},
{
Query: `SELECT '{"NULL":7}'::jsonb #> '{"NULL"}';`,
Expected: []sql.Row{{`7`}},
},
},
},
{
Name: "jsonb_extract_path returns NULL for NULL path elements",
Assertions: []ScriptTestAssertion{
{
// NULL as the trailing element.
Query: `SELECT '{"a":{"b":42}}'::jsonb #> ARRAY['a',NULL];`,
Expected: []sql.Row{{nil}},
},
{
// NULL as the leading element.
Query: `SELECT '{"a":{"b":42}}'::jsonb #> ARRAY[NULL,'b'];`,
Expected: []sql.Row{{nil}},
},
{
// NULL element in the middle of an otherwise valid path.
Query: `SELECT '{"a":{"b":42}}'::jsonb #> ARRAY['a',NULL,'b'];`,
Expected: []sql.Row{{nil}},
},
{
// Unquoted NULL in the '{...}' literal is a SQL NULL element.
Query: `SELECT '{"a":{"b":42}}'::jsonb #> '{a,NULL,b}';`,
Expected: []sql.Row{{nil}},
},
{
// A single unquoted NULL element, even when a key named
// "NULL" exists, still yields NULL.
Query: `SELECT '{"NULL":7}'::jsonb #> '{NULL}';`,
Expected: []sql.Row{{nil}},
},
{
// The text-returning #>> variant behaves the same way.
Query: `SELECT '{"a":{"b":42}}'::jsonb #>> ARRAY['a',NULL];`,
Expected: []sql.Row{{nil}},
},
{
// A NULL array operand (vs. a NULL element) is NULL via the
// function being strict.
Query: `SELECT '{"a":{"b":42}}'::jsonb #> NULL::text[];`,
Expected: []sql.Row{{nil}},
},
},
},
{
// The json (non-binary) variants resolve through json_extract_path /
// json_extract_path_text and must match the jsonb behavior above.
Name: "json_extract_path with text-array paths and NULL elements",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":{"b":42}}'::json #> ARRAY['a','b'];`,
Expected: []sql.Row{{`42`}},
},
{
Query: `SELECT '{"a":{"b":42}}'::json #>> ARRAY['a','b'];`,
Expected: []sql.Row{{`42`}},
},
{
Query: `SELECT '{"a":{"b":42}}'::json #> ARRAY['a',NULL];`,
Expected: []sql.Row{{nil}},
},
{
Query: `SELECT '{"a":{"b":42}}'::json #>> ARRAY[NULL,'b'];`,
Expected: []sql.Row{{nil}},
},
},
},
})
}
// TestJsonExists exercises the `?`, `?|`, and `?&` operators
// (jsonb_exists / jsonb_exists_any / jsonb_exists_all). For object operands
// the optimized path tests for the key via types.LookupJSONValue; for arrays
// and scalars the existing materialized check is used.
func TestJsonExists(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "jsonb_exists (?) tests key/element presence",
Assertions: []ScriptTestAssertion{
{
// Object: key exists.
Query: `SELECT '{"a":1,"b":2}'::jsonb ? 'a';`,
Expected: []sql.Row{{"t"}},
},
{
// Object: missing key.
Query: `SELECT '{"a":1,"b":2}'::jsonb ? 'z';`,
Expected: []sql.Row{{"f"}},
},
{
// Object: key whose value is JSON null still counts as
// existing.
Query: `SELECT '{"a":null}'::jsonb ? 'a';`,
Expected: []sql.Row{{"t"}},
},
{
// Array: text matches a string element.
Query: `SELECT '["alpha","beta","gamma"]'::jsonb ? 'beta';`,
Expected: []sql.Row{{"t"}},
},
{
// Array: text does not match any element.
Query: `SELECT '["alpha","beta"]'::jsonb ? 'gamma';`,
Expected: []sql.Row{{"f"}},
},
{
// Array: matching only on string elements, not numbers.
Query: `SELECT '[1,2,3]'::jsonb ? '1';`,
Expected: []sql.Row{{"f"}},
},
{
// Scalar string equality.
Query: `SELECT '"hello"'::jsonb ? 'hello';`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT '"hello"'::jsonb ? 'world';`,
Expected: []sql.Row{{"f"}},
},
{
// Non-string scalar never matches.
Query: `SELECT '42'::jsonb ? '42';`,
Expected: []sql.Row{{"f"}},
},
},
},
{
Name: "jsonb_exists_any (?|) tests presence of any key",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":1,"b":2}'::jsonb ?| ARRAY['x','b'];`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT '{"a":1,"b":2}'::jsonb ?| ARRAY['x','y'];`,
Expected: []sql.Row{{"f"}},
},
{
Query: `SELECT '["a","b","c"]'::jsonb ?| ARRAY['x','b'];`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT '["a","b","c"]'::jsonb ?| ARRAY['x','y'];`,
Expected: []sql.Row{{"f"}},
},
},
},
{
Name: "jsonb_exists_all (?&) tests presence of all keys",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{"a":1,"b":2,"c":3}'::jsonb ?& ARRAY['a','b'];`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT '{"a":1,"b":2}'::jsonb ?& ARRAY['a','missing'];`,
Expected: []sql.Row{{"f"}},
},
{
Query: `SELECT '["a","b","c"]'::jsonb ?& ARRAY['a','b'];`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT '["a","b"]'::jsonb ?& ARRAY['a','missing'];`,
Expected: []sql.Row{{"f"}},
},
},
},
})
}
// TestJsonLargeDocumentAccess exercises the same operators against JSONB
// values that are stored in a table column. Documents that exceed ~4 KB are
// stored as out-of-band IndexedJsonDocument values by Dolt's storage layer,
// which implements the SearchableJSON and ComparableJSON interfaces; this
// test ensures the optimized lookup paths in jsonb_object_field,
// jsonb_array_element, jsonb_extract_path, and jsonb_exists* still produce
// correct results when fed through the indexed representation.
func TestJsonLargeDocumentAccess(t *testing.T) {
largeObj := makeLargeJSONObject(100) // ~8 KB
largeArr := makeLargeJSONArray(80) // ~5 KB
RunScripts(t, []ScriptTest{
{
Name: "JSONB operators on large stored object (>4 KB)",
SetUpScript: []string{
`CREATE TABLE bigobj (id INT PRIMARY KEY, doc JSONB)`,
`INSERT INTO bigobj (id, doc) VALUES (1, '` + largeObj + `'::jsonb)`,
},
Assertions: []ScriptTestAssertion{
{
// Sanity check: the stored document is larger than 4 KB,
// which exercises the indexed JSON document path.
Query: `SELECT length(doc::text) > 4096 FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
// jsonb_object_field on a stored indexed document.
Query: `SELECT doc -> 'k_0037' ->> 'name' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`value_0037`}},
},
{
// First key at the start of the document.
Query: `SELECT doc -> 'k_0000' ->> 'name' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`value_0000`}},
},
{
// Last key at the end of the document.
Query: `SELECT doc -> 'k_0099' ->> 'name' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`value_0099`}},
},
{
// Missing key returns SQL NULL.
Query: `SELECT doc -> 'no_such_key' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{nil}},
},
{
// Numeric value via ->>.
Query: `SELECT doc -> 'k_0042' ->> 'n' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`42`}},
},
{
// jsonb_extract_path through several levels of an indexed
// document, ending at an array element.
Query: `SELECT doc #>> '{k_0010, tags, 2}' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`tag-c`}},
},
{
// jsonb_extract_path with a negative index hits the
// negative-index fallback path inside extractOneJsonPathStep.
Query: `SELECT doc #>> '{k_0050, tags, -1}' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{`tag-e`}},
},
{
// Missing intermediate path returns SQL NULL.
Query: `SELECT doc #> '{k_0001, missing}' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{nil}},
},
{
// jsonb_exists on a stored indexed document.
Query: `SELECT doc ? 'k_0017' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT doc ? 'no_such_key' FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"f"}},
},
{
// jsonb_exists_any with a mix of present and missing keys.
Query: `SELECT doc ?| ARRAY['no_such_key', 'k_0005'] FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT doc ?| ARRAY['nope_1', 'nope_2'] FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"f"}},
},
{
// jsonb_exists_all where every key is present.
Query: `SELECT doc ?& ARRAY['k_0001', 'k_0099'] FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
Query: `SELECT doc ?& ARRAY['k_0001', 'no_such_key'] FROM bigobj WHERE id = 1;`,
Expected: []sql.Row{{"f"}},
},
},
},
{
Name: "JSONB operators on large stored array (>4 KB)",
SetUpScript: []string{
`CREATE TABLE bigarr (id INT PRIMARY KEY, doc JSONB)`,
`INSERT INTO bigarr (id, doc) VALUES (1, '` + largeArr + `'::jsonb)`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT length(doc::text) > 4096 FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
// Positive index hits the SearchableJSON fast path.
Query: `SELECT doc -> 17 ->> 'label' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`row_0017`}},
},
{
// First element.
Query: `SELECT doc -> 0 ->> 'label' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`row_0000`}},
},
{
// Last element via positive index.
Query: `SELECT doc -> 79 ->> 'label' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`row_0079`}},
},
{
// Negative index hits the materialized fallback path,
// which must agree with the optimized path on the answer.
Query: `SELECT doc -> -1 ->> 'label' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`row_0079`}},
},
{
// Out-of-range positive index.
Query: `SELECT doc -> 1000 FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{nil}},
},
{
// jsonb_extract_path on an array followed by an object key.
Query: `SELECT doc #>> '{42, label}' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`row_0042`}},
},
{
// jsonb_extract_path into a nested array element.
Query: `SELECT doc #>> '{42, payload, 3}' FROM bigarr WHERE id = 1;`,
Expected: []sql.Row{{`d`}},
},
},
},
{
Name: "jsonb_extract_path on large stored object with numeric keys (>4 KB)",
SetUpScript: []string{
`CREATE TABLE numkeys (id INT PRIMARY KEY, doc JSONB)`,
`INSERT INTO numkeys (id, doc) VALUES (1, '` + makeLargeJSONObjectWithNumericKeys() + `'::jsonb)`,
},
Assertions: []ScriptTestAssertion{
{
Query: `SELECT length(doc::text) > 4096 FROM numkeys WHERE id = 1;`,
Expected: []sql.Row{{"t"}},
},
{
// A numeric path element on an object is first guessed as an
// array index ([0]), which the indexed lookup rejects, so it
// must fall back to the object key "0".
Query: `SELECT doc #>> '{nums, 0}' FROM numkeys WHERE id = 1;`,
Expected: []sql.Row{{`zero`}},
},
{
Query: `SELECT doc #>> '{nums, 2}' FROM numkeys WHERE id = 1;`,
Expected: []sql.Row{{`two`}},
},
{
// Missing numeric key returns SQL NULL after the fallback.
Query: `SELECT doc #> '{nums, 5}' FROM numkeys WHERE id = 1;`,
Expected: []sql.Row{{nil}},
},
{
// A genuine object key + array index path through the same
// large document still resolves on the single-lookup path.
Query: `SELECT doc #>> '{k_0001, tags, 0}' FROM numkeys WHERE id = 1;`,
Expected: []sql.Row{{`tag-a`}},
},
},
},
})
}
// TestJsonbNumericCasts exercises the jsonb → numeric type casts in
// server/cast/jsonb.go. The integer casts must round half-to-even (matching
// Postgres' numeric → integer rules) and return an out-of-range error when
// the rounded value doesn't fit in the destination type. The float casts
// must reject values too large to represent as a finite value in the
// destination floating-point type. The non-numeric jsonb cases (object,
// array, string, boolean, null) must each error with a type-specific
// message.
func TestJsonbNumericCasts(t *testing.T) {
RunScripts(t, []ScriptTest{
{
Name: "jsonb -> int2: rounding, boundaries, and out-of-range",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '12345'::jsonb::int2;`,
Expected: []sql.Row{{int16(12345)}},
},
{
Query: `SELECT '-12345'::jsonb::int2;`,
Expected: []sql.Row{{int16(-12345)}},
},
{
// Half-to-even rounding: 0.4 always rounds down.
Query: `SELECT '12345.4'::jsonb::int2;`,
Expected: []sql.Row{{int16(12345)}},
},
{
// 12345.5 → 12346 (round half to even, 12346 is even).
Query: `SELECT '12345.5'::jsonb::int2;`,
Expected: []sql.Row{{int16(12346)}},
},
{
// 12346.5 → 12346 (round half to even, 12346 is even).
Query: `SELECT '12346.5'::jsonb::int2;`,
Expected: []sql.Row{{int16(12346)}},
},
{
// Boundary values that fit exactly.
Query: `SELECT '32767'::jsonb::int2;`,
Expected: []sql.Row{{int16(32767)}},
},
{
Query: `SELECT '-32768'::jsonb::int2;`,
Expected: []sql.Row{{int16(-32768)}},
},
{
// Fractional value that rounds down into range.
Query: `SELECT '32767.4'::jsonb::int2;`,
Expected: []sql.Row{{int16(32767)}},
},
{
// One past the upper bound.
Query: `SELECT '32768'::jsonb::int2;`,
ExpectedErr: "smallint out of range",
},
{
// 32767.5 rounds to 32768, which is out of range.
Query: `SELECT '32767.5'::jsonb::int2;`,
ExpectedErr: "smallint out of range",
},
{
Query: `SELECT '-32769'::jsonb::int2;`,
ExpectedErr: "smallint out of range",
},
{
// Values far outside the int16 range still produce a
// clean out-of-range error rather than an int64 overflow.
Query: `SELECT '1e20'::jsonb::int2;`,
ExpectedErr: "smallint out of range",
},
},
},
{
Name: "jsonb -> int4: rounding, boundaries, and out-of-range",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '0'::jsonb::int4;`,
Expected: []sql.Row{{int32(0)}},
},
{
Query: `SELECT '2147483647'::jsonb::int4;`,
Expected: []sql.Row{{int32(2147483647)}},
},
{
Query: `SELECT '-2147483648'::jsonb::int4;`,
Expected: []sql.Row{{int32(-2147483648)}},
},
{
// Fractional that rounds down into range.
Query: `SELECT '2147483647.4'::jsonb::int4;`,
Expected: []sql.Row{{int32(2147483647)}},
},
{
Query: `SELECT '2147483648'::jsonb::int4;`,
ExpectedErr: "integer out of range",
},
{
Query: `SELECT '-2147483649'::jsonb::int4;`,
ExpectedErr: "integer out of range",
},
{
Query: `SELECT '1e20'::jsonb::int4;`,
ExpectedErr: "integer out of range",
},
},
},
{
Name: "jsonb -> int8: rounding, boundaries, and out-of-range",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '0'::jsonb::int8;`,
Expected: []sql.Row{{int64(0)}},
},
{
// 2^53 - 1: the largest integer that survives the
// round-trip through float64 that the jsonb parser
// currently performs on input.
Query: `SELECT '9007199254740991'::jsonb::int8;`,
Expected: []sql.Row{{int64(9007199254740991)}},
},
{
Query: `SELECT '-9007199254740991'::jsonb::int8;`,
Expected: []sql.Row{{int64(-9007199254740991)}},
},
{
// Large value that doesn't fit in int64 must error
// rather than silently truncating.
Query: `SELECT '1e20'::jsonb::int8;`,
ExpectedErr: "bigint out of range",
},
{
Query: `SELECT '-1e20'::jsonb::int8;`,
ExpectedErr: "bigint out of range",
},
},
},
{
Name: "jsonb -> float4: out-of-range",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '0'::jsonb::float4;`,
Expected: []sql.Row{{float32(0)}},
},
{
Query: `SELECT '1.5'::jsonb::float4;`,
Expected: []sql.Row{{float32(1.5)}},
},
{
// Just inside float32 max (~3.4028235e38).
Query: `SELECT '3.4e38'::jsonb::float4;`,
Expected: []sql.Row{{float32(3.4e38)}},
},
{
// Just outside float32 max.
Query: `SELECT '3.5e38'::jsonb::float4;`,
ExpectedErr: "out of range",
},
{
Query: `SELECT '-3.5e38'::jsonb::float4;`,
ExpectedErr: "out of range",
},
{
Query: `SELECT '1e40'::jsonb::float4;`,
ExpectedErr: "out of range",
},
},
},
{
Name: "jsonb -> float8 round-trips finite values",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '0'::jsonb::float8;`,
Expected: []sql.Row{{float64(0)}},
},
{
Query: `SELECT '1.5'::jsonb::float8;`,
Expected: []sql.Row{{float64(1.5)}},
},
{
// Larger value that still fits in float64.
Query: `SELECT '1e300'::jsonb::float8;`,
Expected: []sql.Row{{float64(1e300)}},
},
// Out-of-range float8 values can't be tested via a jsonb
// literal: the jsonb parser itself rejects '1e400' because
// it cannot be represented in the float64 used for input
// parsing.
},
},
{
Name: "jsonb -> numeric: preserves precision",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '12345'::jsonb::numeric;`,
Expected: []sql.Row{{Numeric("12345")}},
},
{
Query: `SELECT '12345.67'::jsonb::numeric;`,
Expected: []sql.Row{{Numeric("12345.67")}},
},
{
Query: `SELECT '-12345.67'::jsonb::numeric;`,
Expected: []sql.Row{{Numeric("-12345.67")}},
},
},
},
{
Name: "jsonb non-numeric values reject numeric casts",
Assertions: []ScriptTestAssertion{
{
Query: `SELECT '{}'::jsonb::int4;`,
ExpectedErr: "cannot cast jsonb object",
},
{
Query: `SELECT '[]'::jsonb::int4;`,
ExpectedErr: "cannot cast jsonb array",
},
{
Query: `SELECT '"42"'::jsonb::int4;`,
ExpectedErr: "cannot cast jsonb string",
},
{
Query: `SELECT 'true'::jsonb::int4;`,
ExpectedErr: "cannot cast jsonb boolean",
},
{
Query: `SELECT 'null'::jsonb::int4;`,
ExpectedErr: "cannot cast jsonb null",
},
{
Query: `SELECT '{}'::jsonb::float4;`,
ExpectedErr: "cannot cast jsonb object",
},
{
Query: `SELECT '[]'::jsonb::numeric;`,
ExpectedErr: "cannot cast jsonb array",
},
},
},
})
}