Skip to content

Commit d035784

Browse files
authored
Add support for postgres json operators ->, ->>, #>, and #>> (#458)
* add support for postgres json selection Signed-off-by: password <rbalajis25@gmail.com> * fix clippy Signed-off-by: password <rbalajis25@gmail.com> * add support for postgres `#>` and `#>>` json operator * fix clippy Signed-off-by: poonai <rbalajis25@gmail.com> * resolve comments Signed-off-by: password <rbalajis25@gmail.com>
1 parent 8f207db commit d035784

File tree

4 files changed

+174
-2
lines changed

4 files changed

+174
-2
lines changed

src/ast/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,39 @@ impl fmt::Display for Array {
180180
}
181181
}
182182

183+
/// JsonOperator
184+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
185+
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
186+
pub enum JsonOperator {
187+
/// -> keeps the value as json
188+
Arrow,
189+
/// ->> keeps the value as text or int.
190+
LongArrow,
191+
/// #> Extracts JSON sub-object at the specified path
192+
HashArrow,
193+
/// #>> Extracts JSON sub-object at the specified path as text
194+
HashLongArrow,
195+
}
196+
197+
impl fmt::Display for JsonOperator {
198+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199+
match self {
200+
JsonOperator::Arrow => {
201+
write!(f, "->")
202+
}
203+
JsonOperator::LongArrow => {
204+
write!(f, "->>")
205+
}
206+
JsonOperator::HashArrow => {
207+
write!(f, "#>")
208+
}
209+
JsonOperator::HashLongArrow => {
210+
write!(f, "#>>")
211+
}
212+
}
213+
}
214+
}
215+
183216
/// An SQL expression of any type.
184217
///
185218
/// The parser does not distinguish between expressions of different types
@@ -192,6 +225,12 @@ pub enum Expr {
192225
Identifier(Ident),
193226
/// Multi-part identifier, e.g. `table_alias.column` or `schema.table.col`
194227
CompoundIdentifier(Vec<Ident>),
228+
/// JSON access (postgres) eg: data->'tags'
229+
JsonAccess {
230+
left: Box<Expr>,
231+
operator: JsonOperator,
232+
right: Box<Expr>,
233+
},
195234
/// `IS NULL` operator
196235
IsNull(Box<Expr>),
197236
/// `IS NOT NULL` operator
@@ -507,6 +546,13 @@ impl fmt::Display for Expr {
507546
Expr::Array(set) => {
508547
write!(f, "{}", set)
509548
}
549+
Expr::JsonAccess {
550+
left,
551+
operator,
552+
right,
553+
} => {
554+
write!(f, "{} {} {}", left, operator, right)
555+
}
510556
}
511557
}
512558
}

src/parser.rs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,23 @@ impl<'a> Parser<'a> {
11461146
return self.parse_array_index(expr);
11471147
}
11481148
self.parse_map_access(expr)
1149+
} else if Token::Arrow == tok
1150+
|| Token::LongArrow == tok
1151+
|| Token::HashArrow == tok
1152+
|| Token::HashLongArrow == tok
1153+
{
1154+
let operator = match tok {
1155+
Token::Arrow => JsonOperator::Arrow,
1156+
Token::LongArrow => JsonOperator::LongArrow,
1157+
Token::HashArrow => JsonOperator::HashArrow,
1158+
Token::HashLongArrow => JsonOperator::HashLongArrow,
1159+
_ => unreachable!(),
1160+
};
1161+
Ok(Expr::JsonAccess {
1162+
left: Box::new(expr),
1163+
operator,
1164+
right: Box::new(self.parse_expr()?),
1165+
})
11491166
} else {
11501167
// Can only happen if `get_next_precedence` got out of sync with this function
11511168
parser_err!(format!("No infix parser for token {:?}", tok))
@@ -1291,7 +1308,11 @@ impl<'a> Parser<'a> {
12911308
Token::Mul | Token::Div | Token::Mod | Token::StringConcat => Ok(40),
12921309
Token::DoubleColon => Ok(50),
12931310
Token::ExclamationMark => Ok(50),
1294-
Token::LBracket => Ok(50),
1311+
Token::LBracket
1312+
| Token::LongArrow
1313+
| Token::Arrow
1314+
| Token::HashArrow
1315+
| Token::HashLongArrow => Ok(50),
12951316
_ => Ok(0),
12961317
}
12971318
}

src/tokenizer.rs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ pub enum Token {
141141
PGCubeRoot,
142142
/// `?` or `$` , a prepared statement arg placeholder
143143
Placeholder(String),
144+
/// ->, used as a operator to extract json field in PostgreSQL
145+
Arrow,
146+
/// ->>, used as a operator to extract json field as text in PostgreSQL
147+
LongArrow,
148+
/// #> Extracts JSON sub-object at the specified path
149+
HashArrow,
150+
/// #>> Extracts JSON sub-object at the specified path as text
151+
HashLongArrow,
144152
}
145153

146154
impl fmt::Display for Token {
@@ -197,6 +205,10 @@ impl fmt::Display for Token {
197205
Token::PGSquareRoot => f.write_str("|/"),
198206
Token::PGCubeRoot => f.write_str("||/"),
199207
Token::Placeholder(ref s) => write!(f, "{}", s),
208+
Token::Arrow => write!(f, "->"),
209+
Token::LongArrow => write!(f, "->>"),
210+
Token::HashArrow => write!(f, "#>"),
211+
Token::HashLongArrow => write!(f, "#>>"),
200212
}
201213
}
202214
}
@@ -483,6 +495,16 @@ impl<'a> Tokenizer<'a> {
483495
comment,
484496
})))
485497
}
498+
Some('>') => {
499+
chars.next();
500+
match chars.peek() {
501+
Some('>') => {
502+
chars.next();
503+
Ok(Some(Token::LongArrow))
504+
}
505+
_ => Ok(Some(Token::Arrow)),
506+
}
507+
}
486508
// a regular '-' operator
487509
_ => Ok(Some(Token::Minus)),
488510
}
@@ -600,7 +622,22 @@ impl<'a> Tokenizer<'a> {
600622
_ => Ok(Some(Token::Tilde)),
601623
}
602624
}
603-
'#' => self.consume_and_return(chars, Token::Sharp),
625+
'#' => {
626+
chars.next();
627+
match chars.peek() {
628+
Some('>') => {
629+
chars.next();
630+
match chars.peek() {
631+
Some('>') => {
632+
chars.next();
633+
Ok(Some(Token::HashLongArrow))
634+
}
635+
_ => Ok(Some(Token::HashArrow)),
636+
}
637+
}
638+
_ => Ok(Some(Token::Sharp)),
639+
}
640+
}
604641
'@' => self.consume_and_return(chars, Token::AtSign),
605642
'?' => self.consume_and_return(chars, Token::Placeholder(String::from("?"))),
606643
'$' => {

tests/sqlparser_postgres.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1233,6 +1233,74 @@ fn test_savepoint() {
12331233
}
12341234
}
12351235

1236+
#[test]
1237+
fn test_json() {
1238+
let sql = "SELECT params ->> 'name' FROM events";
1239+
let select = pg().verified_only_select(sql);
1240+
assert_eq!(
1241+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1242+
left: Box::new(Expr::Identifier(Ident::new("params"))),
1243+
operator: JsonOperator::LongArrow,
1244+
right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))),
1245+
}),
1246+
select.projection[0]
1247+
);
1248+
1249+
let sql = "SELECT params -> 'name' FROM events";
1250+
let select = pg().verified_only_select(sql);
1251+
assert_eq!(
1252+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1253+
left: Box::new(Expr::Identifier(Ident::new("params"))),
1254+
operator: JsonOperator::Arrow,
1255+
right: Box::new(Expr::Value(Value::SingleQuotedString("name".to_string()))),
1256+
}),
1257+
select.projection[0]
1258+
);
1259+
1260+
let sql = "SELECT info -> 'items' ->> 'product' FROM orders";
1261+
let select = pg().verified_only_select(sql);
1262+
assert_eq!(
1263+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1264+
left: Box::new(Expr::Identifier(Ident::new("info"))),
1265+
operator: JsonOperator::Arrow,
1266+
right: Box::new(Expr::JsonAccess {
1267+
left: Box::new(Expr::Value(Value::SingleQuotedString("items".to_string()))),
1268+
operator: JsonOperator::LongArrow,
1269+
right: Box::new(Expr::Value(Value::SingleQuotedString(
1270+
"product".to_string()
1271+
)))
1272+
}),
1273+
}),
1274+
select.projection[0]
1275+
);
1276+
1277+
let sql = "SELECT info #> '{a,b,c}' FROM orders";
1278+
let select = pg().verified_only_select(sql);
1279+
assert_eq!(
1280+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1281+
left: Box::new(Expr::Identifier(Ident::new("info"))),
1282+
operator: JsonOperator::HashArrow,
1283+
right: Box::new(Expr::Value(Value::SingleQuotedString(
1284+
"{a,b,c}".to_string()
1285+
))),
1286+
}),
1287+
select.projection[0]
1288+
);
1289+
1290+
let sql = "SELECT info #>> '{a,b,c}' FROM orders";
1291+
let select = pg().verified_only_select(sql);
1292+
assert_eq!(
1293+
SelectItem::UnnamedExpr(Expr::JsonAccess {
1294+
left: Box::new(Expr::Identifier(Ident::new("info"))),
1295+
operator: JsonOperator::HashLongArrow,
1296+
right: Box::new(Expr::Value(Value::SingleQuotedString(
1297+
"{a,b,c}".to_string()
1298+
))),
1299+
}),
1300+
select.projection[0]
1301+
);
1302+
}
1303+
12361304
#[test]
12371305
fn parse_comments() {
12381306
match pg().verified_stmt("COMMENT ON COLUMN tab.name IS 'comment'") {

0 commit comments

Comments
 (0)