diff --git a/proto/expr.proto b/proto/expr.proto index 7998f2fe8128a..cab83e0ea45ce 100644 --- a/proto/expr.proto +++ b/proto/expr.proto @@ -222,6 +222,16 @@ message ExprNode { JSONB_CAT = 605; JSONB_OBJECT = 606; JSONB_PRETTY = 607; + // jsonb @> jsonb + JSONB_CONTAINS = 608; + // jsonb <@ jsonb + JSONB_CONTAINED = 609; + // jsonb ? text + JSONB_EXISTS = 610; + // jsonb ?| text[] + JSONB_EXISTS_ANY = 611; + // jsonb ?& text[] + JSONB_EXISTS_ALL = 612; // Non-pure functions below (> 1000) // ------------------------ diff --git a/src/expr/impl/src/scalar/jsonb_contains.rs b/src/expr/impl/src/scalar/jsonb_contains.rs new file mode 100644 index 0000000000000..02dfffe9d2d18 --- /dev/null +++ b/src/expr/impl/src/scalar/jsonb_contains.rs @@ -0,0 +1,273 @@ +// Copyright 2023 RisingWave Labs +// +// 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. + +use jsonbb::ValueRef; +use risingwave_common::types::{JsonbRef, ListRef}; +use risingwave_expr::function; + +/// Does the first JSON value contain the second? +/// +/// Examples: +/// +/// ```slt +/// # Simple scalar/primitive values contain only the identical value: +/// query B +/// SELECT '"foo"'::jsonb @> '"foo"'::jsonb; +/// ---- +/// t +/// +/// # The array on the right side is contained within the one on the left: +/// query B +/// SELECT '[1, 2, 3]'::jsonb @> '[1, 3]'::jsonb; +/// ---- +/// t +/// +/// # Order of array elements is not significant, so this is also true: +/// query B +/// SELECT '[1, 2, 3]'::jsonb @> '[3, 1]'::jsonb; +/// ---- +/// t +/// +/// # Duplicate array elements don't matter either: +/// query B +/// SELECT '[1, 2, 3]'::jsonb @> '[1, 2, 2]'::jsonb; +/// ---- +/// t +/// +/// # The object with a single pair on the right side is contained +/// # within the object on the left side: +/// query B +/// SELECT '{"product": "PostgreSQL", "version": 9.4, "jsonb": true}'::jsonb @> '{"version": 9.4}'::jsonb; +/// ---- +/// t +/// +/// # The array on the right side is not considered contained within the +/// # array on the left, even though a similar array is nested within it: +/// query B +/// SELECT '[1, 2, [1, 3]]'::jsonb @> '[1, 3]'::jsonb; +/// ---- +/// f +/// +/// # But with a layer of nesting, it is contained: +/// query B +/// SELECT '[1, 2, [1, 3]]'::jsonb @> '[[1, 3]]'::jsonb; +/// ---- +/// t +/// +/// # Similarly, containment is not reported here: +/// query B +/// SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"bar": "baz"}'::jsonb; +/// ---- +/// f +/// +/// # A top-level key and an empty object is contained: +/// query B +/// SELECT '{"foo": {"bar": "baz"}}'::jsonb @> '{"foo": {}}'::jsonb; +/// ---- +/// t +/// +/// # This array contains the primitive string value: +/// query B +/// SELECT '["foo", "bar"]'::jsonb @> '"bar"'::jsonb; +/// ---- +/// t +/// +/// # This exception is not reciprocal -- non-containment is reported here: +/// query B +/// SELECT '"bar"'::jsonb @> '["bar"]'::jsonb; +/// ---- +/// f +/// +/// # Object is not primitive: +/// query B +/// SELECT '[1, {"a":2}]'::jsonb @> '{"a":2}'; +/// ---- +/// f +/// +/// # Array can be nested: +/// query B +/// SELECT '[1, [3, 4]]'::jsonb @> '[[3]]'; +/// ---- +/// t +/// +/// # Recursion shall not include the special rule of array containing primitive: +/// query B +/// SELECT '{"a": [3, 4]}'::jsonb @> '{"a": 3}'; +/// ---- +/// f +/// ``` +#[function("jsonb_contains(jsonb, jsonb) -> boolean")] +fn jsonb_contains(left: JsonbRef<'_>, right: JsonbRef<'_>) -> bool { + jsonbb_contains(left.into(), right.into(), true) +} + +/// Performs `jsonb_contains` on `jsonbb::ValueRef`. +/// `root` indicates whether the current recursion is at the root level. +fn jsonbb_contains(left: ValueRef<'_>, right: ValueRef<'_>, root: bool) -> bool { + match (left, right) { + // Both left and right are objects. + (ValueRef::Object(left_obj), ValueRef::Object(right_obj)) => { + // Every key-value pair in right should be present in left. + right_obj.iter().all(|(key, value)| { + left_obj + .get(key) + .map_or(false, |left_val| jsonbb_contains(left_val, value, false)) + }) + } + + // Both left and right are arrays. + (ValueRef::Array(left_arr), ValueRef::Array(right_arr)) => { + // For every value in right, there should be an equivalent in left. + right_arr.iter().all(|right_val| { + left_arr + .iter() + .any(|left_val| jsonbb_contains(left_val, right_val, false)) + }) + } + + // Left is an array and right is an object. + (ValueRef::Array(_), ValueRef::Object(_)) => false, + + // Left is an array and right is a primitive value. only at root level. + (ValueRef::Array(left_arr), right_val) if root => { + // The right should be present in left. + left_arr.iter().any(|left_val| left_val == right_val) + } + + // Both left and right are primitive values. + (left_val, right_val) => left_val == right_val, + } +} + +/// Is the first JSON value contained in the second? +/// +/// Examples: +/// +/// ```slt +/// query B +/// select '{"b":2}'::jsonb <@ '{"a":1, "b":2}'::jsonb; +/// ---- +/// t +/// ``` +#[function("jsonb_contained(jsonb, jsonb) -> boolean")] +fn jsonb_contained(left: JsonbRef<'_>, right: JsonbRef<'_>) -> bool { + jsonb_contains(right, left) +} + +/// Does the text string exist as a top-level key or array element within the JSON value? +/// +/// Examples: +/// +/// ```slt +/// # String exists as array element: +/// query B +/// SELECT '["foo", "bar", "baz"]'::jsonb ? 'bar'; +/// ---- +/// t +/// +/// # String exists as object key: +/// query B +/// SELECT '{"foo": "bar"}'::jsonb ? 'foo'; +/// ---- +/// t +/// +/// # Object values are not considered: +/// query B +/// SELECT '{"foo": "bar"}'::jsonb ? 'bar'; +/// ---- +/// f +/// +/// # As with containment, existence must match at the top level: +/// query B +/// SELECT '{"foo": {"bar": "baz"}}'::jsonb ? 'bar'; +/// ---- +/// f +/// +/// # A string is considered to exist if it matches a primitive JSON string: +/// query B +/// SELECT '"foo"'::jsonb ? 'foo'; +/// ---- +/// t +/// ``` +#[function("jsonb_exists(jsonb, varchar) -> boolean")] +fn jsonb_exists(left: JsonbRef<'_>, key: &str) -> bool { + match left.into() { + ValueRef::Object(object) => object.get(key).is_some(), + ValueRef::Array(array) => array.iter().any(|val| val.as_str() == Some(key)), + ValueRef::String(str) => str == key, + _ => false, + } +} + +/// Do any of the strings in the text array exist as top-level keys or array elements? +/// +/// Examples: +/// +/// ```slt +/// query B +/// select '{"a":1, "b":2, "c":3}'::jsonb ?| array['b', 'd']; +/// ---- +/// t +/// +/// query B +/// select '["a", "b", "c"]'::jsonb ?| array['b', 'd']; +/// ---- +/// t +/// +/// query B +/// select '"b"'::jsonb ?| array['b', 'd']; +/// ---- +/// t +/// ``` +#[function("jsonb_exists_any(jsonb, varchar[]) -> boolean")] +fn jsonb_exists_any(left: JsonbRef<'_>, keys: ListRef<'_>) -> bool { + let mut keys = keys.iter().flatten().map(|val| val.into_utf8()); + match left.into() { + ValueRef::Object(object) => keys.any(|key| object.get(key).is_some()), + ValueRef::Array(array) => keys.any(|key| array.iter().any(|val| val.as_str() == Some(key))), + ValueRef::String(str) => keys.any(|key| str == key), + _ => false, + } +} + +/// Do all of the strings in the text array exist as top-level keys or array elements? +/// +/// Examples: +/// +/// ```slt +/// query B +/// select '{"a":1, "b":2, "c":3}'::jsonb ?& array['a', 'b']; +/// ---- +/// t +/// +/// query B +/// select '["a", "b", "c"]'::jsonb ?& array['a', 'b']; +/// ---- +/// t +/// +/// query B +/// select '"b"'::jsonb ?& array['b']; +/// ---- +/// t +/// ``` +#[function("jsonb_exists_all(jsonb, varchar[]) -> boolean")] +fn jsonb_exists_all(left: JsonbRef<'_>, keys: ListRef<'_>) -> bool { + let mut keys = keys.iter().flatten().map(|val| val.into_utf8()); + match left.into() { + ValueRef::Object(object) => keys.all(|key| object.get(key).is_some()), + ValueRef::Array(array) => keys.all(|key| array.iter().any(|val| val.as_str() == Some(key))), + ValueRef::String(str) => keys.all(|key| str == key), + _ => false, + } +} diff --git a/src/expr/impl/src/scalar/mod.rs b/src/expr/impl/src/scalar/mod.rs index d9d10e4548aee..d1b89d3c75d6c 100644 --- a/src/expr/impl/src/scalar/mod.rs +++ b/src/expr/impl/src/scalar/mod.rs @@ -44,6 +44,7 @@ mod format_type; mod int256; mod jsonb_access; mod jsonb_concat; +mod jsonb_contains; mod jsonb_info; mod jsonb_object; mod length; diff --git a/src/frontend/src/binder/expr/binary_op.rs b/src/frontend/src/binder/expr/binary_op.rs index f7c8a86144fc9..bd85089b18cce 100644 --- a/src/frontend/src/binder/expr/binary_op.rs +++ b/src/frontend/src/binder/expr/binary_op.rs @@ -92,6 +92,11 @@ impl Binder { BinaryOperator::Arrow => ExprType::JsonbAccessInner, BinaryOperator::LongArrow => ExprType::JsonbAccessStr, BinaryOperator::Prefix => ExprType::StartsWith, + BinaryOperator::Contains => ExprType::JsonbContains, + BinaryOperator::Contained => ExprType::JsonbContained, + BinaryOperator::Exists => ExprType::JsonbExists, + BinaryOperator::ExistsAny => ExprType::JsonbExistsAny, + BinaryOperator::ExistsAll => ExprType::JsonbExistsAll, BinaryOperator::Concat => { let left_type = (!bound_left.is_untyped()).then(|| bound_left.return_type()); let right_type = (!bound_right.is_untyped()).then(|| bound_right.return_type()); diff --git a/src/frontend/src/binder/expr/function.rs b/src/frontend/src/binder/expr/function.rs index 50a621c0879d5..7ca509e6e3af9 100644 --- a/src/frontend/src/binder/expr/function.rs +++ b/src/frontend/src/binder/expr/function.rs @@ -879,6 +879,11 @@ impl Binder { ("jsonb_array_length", raw_call(ExprType::JsonbArrayLength)), ("jsonb_object", raw_call(ExprType::JsonbObject)), ("jsonb_pretty", raw_call(ExprType::JsonbPretty)), + ("jsonb_contains", raw_call(ExprType::JsonbContains)), + ("jsonb_contained", raw_call(ExprType::JsonbContained)), + ("jsonb_exists", raw_call(ExprType::JsonbExists)), + ("jsonb_exists_any", raw_call(ExprType::JsonbExistsAny)), + ("jsonb_exists_all", raw_call(ExprType::JsonbExistsAll)), // Functions that return a constant value ("pi", pi()), // greatest and least diff --git a/src/frontend/src/expr/pure.rs b/src/frontend/src/expr/pure.rs index a3c7abf1ef482..038bd94ffcf07 100644 --- a/src/frontend/src/expr/pure.rs +++ b/src/frontend/src/expr/pure.rs @@ -179,6 +179,11 @@ impl ExprVisitor for ImpureAnalyzer { | expr_node::Type::JsonbArrayLength | expr_node::Type::JsonbObject | expr_node::Type::JsonbPretty + | expr_node::Type::JsonbContains + | expr_node::Type::JsonbContained + | expr_node::Type::JsonbExists + | expr_node::Type::JsonbExistsAny + | expr_node::Type::JsonbExistsAll | expr_node::Type::IsJson | expr_node::Type::Sind | expr_node::Type::Cosd diff --git a/src/sqlparser/src/ast/operator.rs b/src/sqlparser/src/ast/operator.rs index ad084a59425b0..147a78d0b7174 100644 --- a/src/sqlparser/src/ast/operator.rs +++ b/src/sqlparser/src/ast/operator.rs @@ -99,6 +99,11 @@ pub enum BinaryOperator { LongArrow, HashArrow, HashLongArrow, + Contains, + Contained, + Exists, + ExistsAny, + ExistsAll, PGQualified(Box), } @@ -143,6 +148,11 @@ impl fmt::Display for BinaryOperator { BinaryOperator::LongArrow => "->>", BinaryOperator::HashArrow => "#>", BinaryOperator::HashLongArrow => "#>>", + BinaryOperator::Contains => "@>", + BinaryOperator::Contained => "<@", + BinaryOperator::Exists => "?", + BinaryOperator::ExistsAny => "?|", + BinaryOperator::ExistsAll => "?&", BinaryOperator::PGQualified(_) => unreachable!(), }) } diff --git a/src/sqlparser/src/parser.rs b/src/sqlparser/src/parser.rs index d87488ac30648..d73ec295b0f90 100644 --- a/src/sqlparser/src/parser.rs +++ b/src/sqlparser/src/parser.rs @@ -1397,6 +1397,11 @@ impl Parser { Token::LongArrow => Some(BinaryOperator::LongArrow), Token::HashArrow => Some(BinaryOperator::HashArrow), Token::HashLongArrow => Some(BinaryOperator::HashLongArrow), + Token::AtArrow => Some(BinaryOperator::Contains), + Token::ArrowAt => Some(BinaryOperator::Contained), + Token::QuestionMark => Some(BinaryOperator::Exists), + Token::QuestionMarkPipe => Some(BinaryOperator::ExistsAny), + Token::QuestionMarkAmpersand => Some(BinaryOperator::ExistsAll), Token::Word(w) => match w.keyword { Keyword::AND => Some(BinaryOperator::And), Keyword::OR => Some(BinaryOperator::Or), @@ -1735,7 +1740,12 @@ impl Parser { | Token::Arrow | Token::LongArrow | Token::HashArrow - | Token::HashLongArrow => Ok(P::Other), + | Token::HashLongArrow + | Token::AtArrow + | Token::ArrowAt + | Token::QuestionMark + | Token::QuestionMarkPipe + | Token::QuestionMarkAmpersand => Ok(P::Other), Token::Word(w) if w.keyword == Keyword::OPERATOR && self.peek_nth_token(1) == Token::LParen => { diff --git a/src/sqlparser/src/tokenizer.rs b/src/sqlparser/src/tokenizer.rs index d0d1e096f8f73..914f0e5f0f8d4 100644 --- a/src/sqlparser/src/tokenizer.rs +++ b/src/sqlparser/src/tokenizer.rs @@ -164,6 +164,16 @@ pub enum Token { HashArrow, /// `#>>`, extract JSON sub-object at the specified path as text in PostgreSQL HashLongArrow, + /// `@>`, does the left JSON value contain the right JSON path/value entries at the top level + AtArrow, + /// `<@`, does the right JSON value contain the left JSON path/value entries at the top level + ArrowAt, + /// `?`, does the string exist as a top-level key within the JSON value + QuestionMark, + /// `?|`, do any of the strings exist as top-level keys or array elements? + QuestionMarkPipe, + /// `?&`, do all of the strings exist as top-level keys or array elements? + QuestionMarkAmpersand, } impl fmt::Display for Token { @@ -231,6 +241,11 @@ impl fmt::Display for Token { Token::LongArrow => f.write_str("->>"), Token::HashArrow => f.write_str("#>"), Token::HashLongArrow => f.write_str("#>>"), + Token::AtArrow => f.write_str("@>"), + Token::ArrowAt => f.write_str("<@"), + Token::QuestionMark => f.write_str("?"), + Token::QuestionMarkPipe => f.write_str("?|"), + Token::QuestionMarkAmpersand => f.write_str("?&"), } } } @@ -693,6 +708,7 @@ impl<'a> Tokenizer<'a> { } Some('>') => self.consume_and_return(chars, Token::Neq), Some('<') => self.consume_and_return(chars, Token::ShiftLeft), + Some('@') => self.consume_and_return(chars, Token::ArrowAt), _ => Ok(Some(Token::Lt)), } } @@ -759,7 +775,23 @@ impl<'a> Tokenizer<'a> { _ => Ok(Some(Token::Sharp)), } } - '@' => self.consume_and_return(chars, Token::AtSign), + '@' => { + chars.next(); // consume the '@' + match chars.peek() { + Some('>') => self.consume_and_return(chars, Token::AtArrow), + // a regular '@' operator + _ => Ok(Some(Token::AtSign)), + } + } + '?' => { + chars.next(); // consume the '?' + match chars.peek() { + Some('|') => self.consume_and_return(chars, Token::QuestionMarkPipe), + Some('&') => self.consume_and_return(chars, Token::QuestionMarkAmpersand), + // a regular '?' operator + _ => Ok(Some(Token::QuestionMark)), + } + } other => self.consume_and_return(chars, Token::Char(other)), }, None => Ok(None), diff --git a/src/tests/regress/data/sql/jsonb.sql b/src/tests/regress/data/sql/jsonb.sql index 59b00932db189..26c25af897bf4 100644 --- a/src/tests/regress/data/sql/jsonb.sql +++ b/src/tests/regress/data/sql/jsonb.sql @@ -211,53 +211,53 @@ select '"foo"'::jsonb ->> 'z'; --@ SELECT '{"x":"y"}'::jsonb <> '{"x":"z"}'::jsonb; -- containment ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b"}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "c":null}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "g":null}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"g":null}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"c"}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b"}'); ---@ SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "c":"q"}'); ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b"}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "c":null}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "g":null}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"g":null}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"c"}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b"}'; ---@ SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "c":"q"}'; ---@ ---@ SELECT '[1,2]'::jsonb @> '[1,2,2]'::jsonb; ---@ SELECT '[1,1,2]'::jsonb @> '[1,2,2]'::jsonb; ---@ SELECT '[[1,2]]'::jsonb @> '[[1,2,2]]'::jsonb; ---@ SELECT '[1,2,2]'::jsonb <@ '[1,2]'::jsonb; ---@ SELECT '[1,2,2]'::jsonb <@ '[1,1,2]'::jsonb; ---@ SELECT '[[1,2,2]]'::jsonb <@ '[[1,2]]'::jsonb; ---@ ---@ SELECT jsonb_contained('{"a":"b"}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"a":"b", "c":null}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"a":"b", "g":null}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"g":null}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"a":"c"}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"a":"b"}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT jsonb_contained('{"a":"b", "c":"q"}', '{"a":"b", "b":1, "c":null}'); ---@ SELECT '{"a":"b"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"a":"b", "c":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"a":"b", "g":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"g":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"a":"c"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"a":"b"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ SELECT '{"a":"b", "c":"q"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; ---@ -- Raw scalar may contain another raw scalar, array may contain a raw scalar ---@ SELECT '[5]'::jsonb @> '[5]'; ---@ SELECT '5'::jsonb @> '5'; ---@ SELECT '[5]'::jsonb @> '5'; ---@ -- But a raw scalar cannot contain an array ---@ SELECT '5'::jsonb @> '[5]'; ---@ -- In general, one thing should always contain itself. Test array containment: ---@ SELECT '["9", ["7", "3"], 1]'::jsonb @> '["9", ["7", "3"], 1]'::jsonb; ---@ SELECT '["9", ["7", "3"], ["1"]]'::jsonb @> '["9", ["7", "3"], ["1"]]'::jsonb; ---@ -- array containment string matching confusion bug ---@ SELECT '{ "name": "Bob", "tags": [ "enim", "qui"]}'::jsonb @> '{"tags":["qu"]}'; +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b"}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "c":null}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "g":null}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"g":null}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"c"}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b"}'); +SELECT jsonb_contains('{"a":"b", "b":1, "c":null}', '{"a":"b", "c":"q"}'); +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b"}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "c":null}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "g":null}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"g":null}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"c"}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b"}'; +SELECT '{"a":"b", "b":1, "c":null}'::jsonb @> '{"a":"b", "c":"q"}'; + +SELECT '[1,2]'::jsonb @> '[1,2,2]'::jsonb; +SELECT '[1,1,2]'::jsonb @> '[1,2,2]'::jsonb; +SELECT '[[1,2]]'::jsonb @> '[[1,2,2]]'::jsonb; +SELECT '[1,2,2]'::jsonb <@ '[1,2]'::jsonb; +SELECT '[1,2,2]'::jsonb <@ '[1,1,2]'::jsonb; +SELECT '[[1,2,2]]'::jsonb <@ '[[1,2]]'::jsonb; + +SELECT jsonb_contained('{"a":"b"}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"a":"b", "c":null}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"a":"b", "g":null}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"g":null}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"a":"c"}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"a":"b"}', '{"a":"b", "b":1, "c":null}'); +SELECT jsonb_contained('{"a":"b", "c":"q"}', '{"a":"b", "b":1, "c":null}'); +SELECT '{"a":"b"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"a":"b", "c":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"a":"b", "g":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"g":null}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"a":"c"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"a":"b"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +SELECT '{"a":"b", "c":"q"}'::jsonb <@ '{"a":"b", "b":1, "c":null}'; +-- Raw scalar may contain another raw scalar, array may contain a raw scalar +SELECT '[5]'::jsonb @> '[5]'; +SELECT '5'::jsonb @> '5'; +SELECT '[5]'::jsonb @> '5'; +-- But a raw scalar cannot contain an array +SELECT '5'::jsonb @> '[5]'; +-- In general, one thing should always contain itself. Test array containment: +SELECT '["9", ["7", "3"], 1]'::jsonb @> '["9", ["7", "3"], 1]'::jsonb; +SELECT '["9", ["7", "3"], ["1"]]'::jsonb @> '["9", ["7", "3"], ["1"]]'::jsonb; +-- array containment string matching confusion bug +SELECT '{ "name": "Bob", "tags": [ "enim", "qui"]}'::jsonb @> '{"tags":["qu"]}'; -- array length SELECT jsonb_array_length('[1,2,3,{"f1":1,"f2":[5,6]},4]'); @@ -277,14 +277,14 @@ SELECT jsonb_array_length('4'); --@ SELECT * FROM jsonb_each_text('{"a":{"b":"c","c":"b","1":"first"},"b":[1,2],"c":"cc","1":"first","n":null}'::jsonb) AS q; -- exists ---@ SELECT jsonb_exists('{"a":null, "b":"qq"}', 'a'); ---@ SELECT jsonb_exists('{"a":null, "b":"qq"}', 'b'); ---@ SELECT jsonb_exists('{"a":null, "b":"qq"}', 'c'); ---@ SELECT jsonb_exists('{"a":"null", "b":"qq"}', 'a'); ---@ SELECT jsonb '{"a":null, "b":"qq"}' ? 'a'; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ? 'b'; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ? 'c'; ---@ SELECT jsonb '{"a":"null", "b":"qq"}' ? 'a'; +SELECT jsonb_exists('{"a":null, "b":"qq"}', 'a'); +SELECT jsonb_exists('{"a":null, "b":"qq"}', 'b'); +SELECT jsonb_exists('{"a":null, "b":"qq"}', 'c'); +SELECT jsonb_exists('{"a":"null", "b":"qq"}', 'a'); +SELECT jsonb '{"a":null, "b":"qq"}' ? 'a'; +SELECT jsonb '{"a":null, "b":"qq"}' ? 'b'; +SELECT jsonb '{"a":null, "b":"qq"}' ? 'c'; +SELECT jsonb '{"a":"null", "b":"qq"}' ? 'a'; --@ -- array exists - array elements should behave as keys --@ SELECT count(*) from testjsonb WHERE j->'array' ? 'bar'; --@ -- type sensitive array exists - should return no rows (since "exists" only @@ -292,29 +292,29 @@ SELECT jsonb_array_length('4'); --@ SELECT count(*) from testjsonb WHERE j->'array' ? '5'::text; --@ -- However, a raw scalar is *contained* within the array --@ SELECT count(*) from testjsonb WHERE j->'array' @> '5'::jsonb; ---@ ---@ SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['a','b']); ---@ SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['b','a']); ---@ SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['c','a']); ---@ SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['c','d']); ---@ SELECT jsonb_exists_any('{"a":null, "b":"qq"}', '{}'::text[]); ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['a','b']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['b','a']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['c','a']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['c','d']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?| '{}'::text[]; ---@ ---@ SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['a','b']); ---@ SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['b','a']); ---@ SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['c','a']); ---@ SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['c','d']); ---@ SELECT jsonb_exists_all('{"a":null, "b":"qq"}', '{}'::text[]); ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['a','b']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['b','a']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['c','a']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['c','d']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['a','a', 'b', 'b', 'b']; ---@ SELECT jsonb '{"a":null, "b":"qq"}' ?& '{}'::text[]; + +SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['a','b']); +SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['b','a']); +SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['c','a']); +SELECT jsonb_exists_any('{"a":null, "b":"qq"}', ARRAY['c','d']); +SELECT jsonb_exists_any('{"a":null, "b":"qq"}', '{}'::text[]); +SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['a','b']; +SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['b','a']; +SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['c','a']; +SELECT jsonb '{"a":null, "b":"qq"}' ?| ARRAY['c','d']; +SELECT jsonb '{"a":null, "b":"qq"}' ?| '{}'::text[]; + +SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['a','b']); +SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['b','a']); +SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['c','a']); +SELECT jsonb_exists_all('{"a":null, "b":"qq"}', ARRAY['c','d']); +SELECT jsonb_exists_all('{"a":null, "b":"qq"}', '{}'::text[]); +SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['a','b']; +SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['b','a']; +SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['c','a']; +SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['c','d']; +SELECT jsonb '{"a":null, "b":"qq"}' ?& ARRAY['a','a', 'b', 'b', 'b']; +SELECT jsonb '{"a":null, "b":"qq"}' ?& '{}'::text[]; -- typeof SELECT jsonb_typeof('{}') AS object; @@ -941,25 +941,25 @@ SELECT '{"ff":{"a":12,"b":16},"qq":123,"x":[1,2],"Y":null}'::jsonb -> 'x'; -- nested containment ---@ SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[1,2]}'; ---@ SELECT '{"a":[2,1],"c":"b"}'::jsonb @> '{"a":[1,2]}'; ---@ SELECT '{"a":{"1":2},"c":"b"}'::jsonb @> '{"a":[1,2]}'; ---@ SELECT '{"a":{"2":1},"c":"b"}'::jsonb @> '{"a":[1,2]}'; ---@ SELECT '{"a":{"1":2},"c":"b"}'::jsonb @> '{"a":{"1":2}}'; ---@ SELECT '{"a":{"2":1},"c":"b"}'::jsonb @> '{"a":{"1":2}}'; ---@ SELECT '["a","b"]'::jsonb @> '["a","b","c","b"]'; ---@ SELECT '["a","b","c","b"]'::jsonb @> '["a","b"]'; ---@ SELECT '["a","b","c",[1,2]]'::jsonb @> '["a",[1,2]]'; ---@ SELECT '["a","b","c",[1,2]]'::jsonb @> '["b",[1,2]]'; ---@ ---@ SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[1]}'; ---@ SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[2]}'; ---@ SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[3]}'; ---@ ---@ SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"c":3}]}'; ---@ SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4}]}'; ---@ SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4},3]}'; ---@ SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4},1]}'; +SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[1,2]}'; +SELECT '{"a":[2,1],"c":"b"}'::jsonb @> '{"a":[1,2]}'; +SELECT '{"a":{"1":2},"c":"b"}'::jsonb @> '{"a":[1,2]}'; +SELECT '{"a":{"2":1},"c":"b"}'::jsonb @> '{"a":[1,2]}'; +SELECT '{"a":{"1":2},"c":"b"}'::jsonb @> '{"a":{"1":2}}'; +SELECT '{"a":{"2":1},"c":"b"}'::jsonb @> '{"a":{"1":2}}'; +SELECT '["a","b"]'::jsonb @> '["a","b","c","b"]'; +SELECT '["a","b","c","b"]'::jsonb @> '["a","b"]'; +SELECT '["a","b","c",[1,2]]'::jsonb @> '["a",[1,2]]'; +SELECT '["a","b","c",[1,2]]'::jsonb @> '["b",[1,2]]'; + +SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[1]}'; +SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[2]}'; +SELECT '{"a":[1,2],"c":"b"}'::jsonb @> '{"a":[3]}'; + +SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"c":3}]}'; +SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4}]}'; +SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4},3]}'; +SELECT '{"a":[1,2,{"c":3,"x":4}],"c":"b"}'::jsonb @> '{"a":[{"x":4},1]}'; -- check some corner cases for indexed nested containment (bug #13756) --@ create temp table nestjsonb (j jsonb); @@ -1020,12 +1020,12 @@ SELECT '["a","b","c",[1,2],null]'::jsonb -> -6; --@ SELECT '[0,1,2,[3,4],{"5":"five"}]'::jsonb #> '{4,5}'; --nested exists ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'n'; ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'a'; ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'b'; ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'c'; ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'd'; ---@ SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'e'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'n'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'a'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'b'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'c'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'd'; +SELECT '{"n":null,"a":1,"b":[1,2],"c":{"1":2},"d":{"1":[2,3]}}'::jsonb ? 'e'; -- jsonb_strip_nulls