Skip to content

Commit 6d6eb4b

Browse files
authored
Support json operators @> <@ #- @? and @@
postgres supports a bunch more json operators. See https://www.postgresql.org/docs/15/functions-json.html Skipping operators starting with a question mark for now, since those are hard to distinguish from placeholders without more context.
1 parent fb02344 commit 6d6eb4b

File tree

4 files changed

+132
-2
lines changed

4 files changed

+132
-2
lines changed

src/ast/mod.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@ pub enum JsonOperator {
190190
HashLongArrow,
191191
/// : Colon is used by Snowflake (Which is similar to LongArrow)
192192
Colon,
193+
/// jsonb @> jsonb -> boolean: Test whether left json contains the right json
194+
AtArrow,
195+
/// jsonb <@ jsonb -> boolean: Test whether right json contains the left json
196+
ArrowAt,
197+
/// jsonb #- text[] -> jsonb: Deletes the field or array element at the specified
198+
/// path, where path elements can be either field keys or array indexes.
199+
HashMinus,
200+
/// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified
201+
/// JSON value?
202+
AtQuestion,
203+
/// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check
204+
/// for the specified JSON value. Only the first item of the result is taken into
205+
/// account. If the result is not Boolean, then NULL is returned.
206+
AtAt,
193207
}
194208

195209
impl fmt::Display for JsonOperator {
@@ -210,6 +224,13 @@ impl fmt::Display for JsonOperator {
210224
JsonOperator::Colon => {
211225
write!(f, ":")
212226
}
227+
JsonOperator::AtArrow => {
228+
write!(f, "@>")
229+
}
230+
JsonOperator::ArrowAt => write!(f, "<@"),
231+
JsonOperator::HashMinus => write!(f, "#-"),
232+
JsonOperator::AtQuestion => write!(f, "@?"),
233+
JsonOperator::AtAt => write!(f, "@@"),
213234
}
214235
}
215236
}

src/parser.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1602,12 +1602,22 @@ impl<'a> Parser<'a> {
16021602
|| Token::LongArrow == tok
16031603
|| Token::HashArrow == tok
16041604
|| Token::HashLongArrow == tok
1605+
|| Token::AtArrow == tok
1606+
|| Token::ArrowAt == tok
1607+
|| Token::HashMinus == tok
1608+
|| Token::AtQuestion == tok
1609+
|| Token::AtAt == tok
16051610
{
16061611
let operator = match tok.token {
16071612
Token::Arrow => JsonOperator::Arrow,
16081613
Token::LongArrow => JsonOperator::LongArrow,
16091614
Token::HashArrow => JsonOperator::HashArrow,
16101615
Token::HashLongArrow => JsonOperator::HashLongArrow,
1616+
Token::AtArrow => JsonOperator::AtArrow,
1617+
Token::ArrowAt => JsonOperator::ArrowAt,
1618+
Token::HashMinus => JsonOperator::HashMinus,
1619+
Token::AtQuestion => JsonOperator::AtQuestion,
1620+
Token::AtAt => JsonOperator::AtAt,
16111621
_ => unreachable!(),
16121622
};
16131623
Ok(Expr::JsonAccess {
@@ -1805,7 +1815,12 @@ impl<'a> Parser<'a> {
18051815
| Token::LongArrow
18061816
| Token::Arrow
18071817
| Token::HashArrow
1808-
| Token::HashLongArrow => Ok(50),
1818+
| Token::HashLongArrow
1819+
| Token::AtArrow
1820+
| Token::ArrowAt
1821+
| Token::HashMinus
1822+
| Token::AtQuestion
1823+
| Token::AtAt => Ok(50),
18091824
_ => Ok(0),
18101825
}
18111826
}

src/tokenizer.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ pub enum Token {
155155
HashArrow,
156156
/// #>> Extracts JSON sub-object at the specified path as text
157157
HashLongArrow,
158+
/// jsonb @> jsonb -> boolean: Test whether left json contains the right json
159+
AtArrow,
160+
/// jsonb <@ jsonb -> boolean: Test whether right json contains the left json
161+
ArrowAt,
162+
/// jsonb #- text[] -> jsonb: Deletes the field or array element at the specified
163+
/// path, where path elements can be either field keys or array indexes.
164+
HashMinus,
165+
/// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified
166+
/// JSON value?
167+
AtQuestion,
168+
/// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check
169+
/// for the specified JSON value. Only the first item of the result is taken into
170+
/// account. If the result is not Boolean, then NULL is returned.
171+
AtAt,
158172
}
159173

160174
impl fmt::Display for Token {
@@ -217,7 +231,12 @@ impl fmt::Display for Token {
217231
Token::LongArrow => write!(f, "->>"),
218232
Token::HashArrow => write!(f, "#>"),
219233
Token::HashLongArrow => write!(f, "#>>"),
234+
Token::AtArrow => write!(f, "@>"),
220235
Token::DoubleDollarQuoting => write!(f, "$$"),
236+
Token::ArrowAt => write!(f, "<@"),
237+
Token::HashMinus => write!(f, "#-"),
238+
Token::AtQuestion => write!(f, "@?"),
239+
Token::AtAt => write!(f, "@@"),
221240
}
222241
}
223242
}
@@ -708,6 +727,7 @@ impl<'a> Tokenizer<'a> {
708727
}
709728
Some('>') => self.consume_and_return(chars, Token::Neq),
710729
Some('<') => self.consume_and_return(chars, Token::ShiftLeft),
730+
Some('@') => self.consume_and_return(chars, Token::ArrowAt),
711731
_ => Ok(Some(Token::Lt)),
712732
}
713733
}
@@ -752,6 +772,7 @@ impl<'a> Tokenizer<'a> {
752772
'#' => {
753773
chars.next();
754774
match chars.peek() {
775+
Some('-') => self.consume_and_return(chars, Token::HashMinus),
755776
Some('>') => {
756777
chars.next();
757778
match chars.peek() {
@@ -765,7 +786,15 @@ impl<'a> Tokenizer<'a> {
765786
_ => Ok(Some(Token::Sharp)),
766787
}
767788
}
768-
'@' => self.consume_and_return(chars, Token::AtSign),
789+
'@' => {
790+
chars.next();
791+
match chars.peek() {
792+
Some('>') => self.consume_and_return(chars, Token::AtArrow),
793+
Some('?') => self.consume_and_return(chars, Token::AtQuestion),
794+
Some('@') => self.consume_and_return(chars, Token::AtAt),
795+
_ => Ok(Some(Token::AtSign)),
796+
}
797+
}
769798
'?' => {
770799
chars.next();
771800
let s = peeking_take_while(chars, |ch| ch.is_numeric());

tests/sqlparser_postgres.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1620,6 +1620,71 @@ fn test_json() {
16201620
}),
16211621
select.projection[0]
16221622
);
1623+
1624+
let sql = "SELECT info FROM orders WHERE info @> '{\"a\": 1}'";
1625+
let select = pg().verified_only_select(sql);
1626+
assert_eq!(
1627+
Expr::JsonAccess {
1628+
left: Box::new(Expr::Identifier(Ident::new("info"))),
1629+
operator: JsonOperator::AtArrow,
1630+
right: Box::new(Expr::Value(Value::SingleQuotedString(
1631+
"{\"a\": 1}".to_string()
1632+
))),
1633+
},
1634+
select.selection.unwrap(),
1635+
);
1636+
1637+
let sql = "SELECT info FROM orders WHERE '{\"a\": 1}' <@ info";
1638+
let select = pg().verified_only_select(sql);
1639+
assert_eq!(
1640+
Expr::JsonAccess {
1641+
left: Box::new(Expr::Value(Value::SingleQuotedString(
1642+
"{\"a\": 1}".to_string()
1643+
))),
1644+
operator: JsonOperator::ArrowAt,
1645+
right: Box::new(Expr::Identifier(Ident::new("info"))),
1646+
},
1647+
select.selection.unwrap(),
1648+
);
1649+
1650+
let sql = "SELECT info #- ARRAY['a', 'b'] FROM orders";
1651+
let select = pg().verified_only_select(sql);
1652+
assert_eq!(
1653+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1654+
left: Box::new(Expr::Identifier(Ident::from("info"))),
1655+
operator: JsonOperator::HashMinus,
1656+
right: Box::new(Expr::Array(Array {
1657+
elem: vec![
1658+
Expr::Value(Value::SingleQuotedString("a".to_string())),
1659+
Expr::Value(Value::SingleQuotedString("b".to_string())),
1660+
],
1661+
named: true,
1662+
})),
1663+
}),
1664+
select.projection[0],
1665+
);
1666+
1667+
let sql = "SELECT info FROM orders WHERE info @? '$.a'";
1668+
let select = pg().verified_only_select(sql);
1669+
assert_eq!(
1670+
Expr::JsonAccess {
1671+
left: Box::new(Expr::Identifier(Ident::from("info"))),
1672+
operator: JsonOperator::AtQuestion,
1673+
right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),),
1674+
},
1675+
select.selection.unwrap(),
1676+
);
1677+
1678+
let sql = "SELECT info FROM orders WHERE info @@ '$.a'";
1679+
let select = pg().verified_only_select(sql);
1680+
assert_eq!(
1681+
Expr::JsonAccess {
1682+
left: Box::new(Expr::Identifier(Ident::from("info"))),
1683+
operator: JsonOperator::AtAt,
1684+
right: Box::new(Expr::Value(Value::SingleQuotedString("$.a".to_string())),),
1685+
},
1686+
select.selection.unwrap(),
1687+
);
16231688
}
16241689

16251690
#[test]

0 commit comments

Comments
 (0)