964 lines
29 KiB
Go
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",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
}
|