Просмотр исходного кода

match PostgreSQL duplicate-insensitive jsonb array containment

Danilo Fragoso 2 дней назад
Родитель
Сommit
883ef93c0e
3 измененных файлов с 12 добавлено и 12 удалено
  1. 1 1
      pkg/capabilities/capabilities.go
  2. 4 6
      pkg/executor/jsonb.go
  3. 7 5
      pkg/executor/jsonb_test.go

+ 1 - 1
pkg/capabilities/capabilities.go

@@ -237,7 +237,7 @@ func Current() *Document {
 			JSONB: JSONB{
 				Canonicalization: "canonical JSON text: object keys sorted byte-wise, duplicate object keys last-wins, no insignificant whitespace, numbers exact normalized decimals (1.50 becomes 1.5, 1e2 becomes 100; no exponent, no trailing fractional zeros, negative zero becomes 0); canonicalization is idempotent and every write path stores the canonical bytes, so a reload reads back the same text",
 				Ordering:         "jsonb equality and comparison operators compare exact canonical text bytes, so key order and number spelling are irrelevant but 1 and 1.0000000000000000001 are distinct and integers above 2^53 are not rounded; DISTINCT and GROUP BY use the canonical value (JSONB columns are canonicalized on read too); ORDER BY follows the engine's existing value comparison on canonical text, where both-numeric pairs compare numerically and every other pair compares byte-wise",
-				Operators:        Feature{Supported: true, Status: "supported", SQLState: "42804", Notes: "-> and ->> extract a member/element ($ path also accepted); #> and #>> extract by a text[] path (or a $ path); @> and <@ test containment with PostgreSQL's object/array/scalar rules; ? tests an object key or top-level string array element; ?| and ?& test a text[] of keys; || merges objects, concatenates arrays, and wraps non-arrays as single-element arrays; - deletes an object key, array index, or text[] of keys/elements; #- deletes a path; NULL operands return NULL, missing keys return SQL NULL (a JSON null member is a jsonb 'null' for -> and SQL NULL for ->>), and @> sees a JSON null member as a value; array containment matches each candidate element to a distinct container element; a text-only @> or <@ is rejected with 42804; - with an integer on an object reports 22023; the HTTP /query API substitutes ? textually, so a statement using the ? operator cannot also use ? positional parameters there"},
+				Operators:        Feature{Supported: true, Status: "supported", SQLState: "42804", Notes: "-> and ->> extract a member/element ($ path also accepted); #> and #>> extract by a text[] path (or a $ path); @> and <@ test containment with PostgreSQL's object/array/scalar rules; ? tests an object key or top-level string array element; ?| and ?& test a text[] of keys; || merges objects, concatenates arrays, and wraps non-arrays as single-element arrays; - deletes an object key, array index, or text[] of keys/elements; #- deletes a path; NULL operands return NULL, missing keys return SQL NULL (a JSON null member is a jsonb 'null' for -> and SQL NULL for ->>), and @> sees a JSON null member as a value; array containment is duplicate-insensitive (each candidate element only needs some matching container element, as in PostgreSQL); a text-only @> or <@ is rejected with 42804; - with an integer on an object reports 22023; the HTTP /query API substitutes ? textually, so a statement using the ? operator cannot also use ? positional parameters there"},
 				Functions:        Feature{Supported: true, Status: "supported", SQLState: "22023", Notes: "jsonb_typeof, jsonb_array_length, jsonb_strip_nulls, jsonb_pretty, jsonb_build_object, jsonb_build_array, to_jsonb, jsonb_set, jsonb_insert; jsonb_set with an empty path leaves the target unchanged, jsonb_set appends a missing array position and clamps a too-low index when create_if_missing is true, jsonb_insert clamps and appends array positions, and jsonb_insert onto an existing object key reports 22023 (cannot replace existing key); the JSON1 jsonb_* aliases (jsonb_set, jsonb_insert, jsonb_replace, jsonb_remove with $ paths) keep their previous value behavior and return canonical jsonb; wrong document kinds report 22023 and malformed input 22P02; the JSON (text) and JSON1 function surface is unchanged"},
 				SetReturning:     Feature{Supported: true, Status: "single_from_item", SQLState: "0A000", Notes: "jsonb_each, jsonb_each_text, jsonb_array_elements, jsonb_array_elements_text, and jsonb_object_keys are supported only as the single FROM item (no joins); calling one in a projection is rejected with 0A000; a NULL document produces no rows"},
 				Parameters:       Feature{Supported: true, Status: "text_and_binary", SQLState: "22P02", Notes: "text-format OID 3802 parameters are canonicalized at Bind; PostgreSQL binary jsonb parameters (version byte 0x01 followed by the document text, which pgx sends by default) are accepted and canonicalized, while a wrong version or malformed body is 22P02; an invalid document fails at Bind and operators see a real jsonb value; OID 114 (JSON) behavior is unchanged"},

+ 4 - 6
pkg/executor/jsonb.go

@@ -961,15 +961,13 @@ func jsonbContains(container, candidate interface{}) bool {
 	case []interface{}:
 		containerArray, ok := container.([]interface{})
 		if ok {
-			used := make([]bool, len(containerArray))
+			// PostgreSQL array containment does not require multiplicity: each
+			// candidate element only needs some container element, and duplicates
+			// may match the same element.
 			for _, candidateElement := range candidateValue {
 				matched := false
-				for i, containerElement := range containerArray {
-					if used[i] {
-						continue
-					}
+				for _, containerElement := range containerArray {
 					if jsonbContains(containerElement, candidateElement) {
-						used[i] = true
 						matched = true
 						break
 					}

+ 7 - 5
pkg/executor/jsonb_test.go

@@ -765,8 +765,6 @@ func TestJSONBExactEqualityAndAggregateAgreement(t *testing.T) {
 	}
 }
 
-// Containment tracks consumed array matches, as required by the review gate:
-// a candidate with duplicates needs a distinct container element per copy.
 // A jsonb extraction result must not be silently compared with a SQL number.
 func TestJSONBExtractionComparisonTypeError(t *testing.T) {
 	_, schema, table := newTestDB(t)
@@ -784,17 +782,21 @@ func TestJSONBExtractionComparisonTypeError(t *testing.T) {
 	}
 }
 
-func TestJSONBArrayContainmentMultiplicity(t *testing.T) {
+// PostgreSQL array containment is duplicate-insensitive: each candidate
+// element only needs some matching container element, and duplicates may
+// consume the same match.
+func TestJSONBArrayContainmentDuplicates(t *testing.T) {
 	_, schema, table := newTestDB(t)
 	e := newExec(schema, table)
 	cases := []struct {
 		sql  string
 		want interface{}
 	}{
-		{`SELECT '[1]'::jsonb @> '[1,1]'::jsonb`, false},
+		{`SELECT '[1]'::jsonb @> '[1,1]'::jsonb`, true},
 		{`SELECT '[1,1]'::jsonb @> '[1]'::jsonb`, true},
 		{`SELECT '[1,2,2]'::jsonb @> '[2,2]'::jsonb`, true},
-		{`SELECT '[1,2]'::jsonb @> '[2,2]'::jsonb`, false},
+		{`SELECT '[1,2]'::jsonb @> '[2,2]'::jsonb`, true},
+		{`SELECT '[1,2]'::jsonb @> '[2,3]'::jsonb`, false},
 		{`SELECT '[1]'::jsonb @> '1'::jsonb`, true},
 	}
 	for _, tc := range cases {