Skip to content

Commit 0b2dc76

Browse files
committed
Support ?-based jsonb operators in Postgres
We already support most postgres jsonb operators, but were missing the ?-based ones. This commit adds support for them in both the tokenizer and the AST enums. This is simplified in the tokenizer with a dialect-specific carve-out, since Postgres thankfully does not also use ? for anonymous prepared statement parameters.
1 parent 547c5cd commit 0b2dc76

File tree

5 files changed

+150
-48
lines changed

5 files changed

+150
-48
lines changed

src/ast/mod.rs

+36-25
Original file line numberDiff line numberDiff line change
@@ -277,28 +277,34 @@ impl fmt::Display for Interval {
277277
pub enum JsonOperator {
278278
/// -> keeps the value as json
279279
Arrow,
280-
/// ->> keeps the value as text or int.
281-
LongArrow,
280+
/// jsonb <@ jsonb -> boolean: Test whether right json contains the left json
281+
ArrowAt,
282+
/// jsonb @> jsonb -> boolean: Test whether left json contains the right json
283+
AtArrow,
284+
/// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check
285+
/// for the specified JSON value. Only the first item of the result is taken into
286+
/// account. If the result is not Boolean, then NULL is returned.
287+
AtAt,
288+
/// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified
289+
/// JSON value?
290+
AtQuestion,
291+
/// : Colon is used by Snowflake (Which is similar to LongArrow)
292+
Colon,
282293
/// #> Extracts JSON sub-object at the specified path
283294
HashArrow,
284295
/// #>> Extracts JSON sub-object at the specified path as text
285296
HashLongArrow,
286-
/// : Colon is used by Snowflake (Which is similar to LongArrow)
287-
Colon,
288-
/// jsonb @> jsonb -> boolean: Test whether left json contains the right json
289-
AtArrow,
290-
/// jsonb <@ jsonb -> boolean: Test whether right json contains the left json
291-
ArrowAt,
292297
/// jsonb #- text[] -> jsonb: Deletes the field or array element at the specified
293298
/// path, where path elements can be either field keys or array indexes.
294299
HashMinus,
295-
/// jsonb @? jsonpath -> boolean: Does JSON path return any item for the specified
296-
/// JSON value?
297-
AtQuestion,
298-
/// jsonb @@ jsonpath → boolean: Returns the result of a JSON path predicate check
299-
/// for the specified JSON value. Only the first item of the result is taken into
300-
/// account. If the result is not Boolean, then NULL is returned.
301-
AtAt,
300+
/// ->> keeps the value as text or int.
301+
LongArrow,
302+
/// jsonb ? string: Does the string exist as a top-level key within the JSON value
303+
Question,
304+
/// jsonb ?& text[]: Do all of these array strings exist as top-level keys?
305+
QuestionAnd,
306+
/// jsonb ?| text[]: Do any of these array strings exist as top-level keys?
307+
QuestionPipe,
302308
}
303309

304310
impl fmt::Display for JsonOperator {
@@ -307,25 +313,30 @@ impl fmt::Display for JsonOperator {
307313
JsonOperator::Arrow => {
308314
write!(f, "->")
309315
}
310-
JsonOperator::LongArrow => {
311-
write!(f, "->>")
316+
JsonOperator::ArrowAt => write!(f, "<@"),
317+
JsonOperator::AtArrow => {
318+
write!(f, "@>")
319+
}
320+
JsonOperator::AtAt => write!(f, "@@"),
321+
JsonOperator::AtQuestion => write!(f, "@?"),
322+
JsonOperator::Colon => {
323+
write!(f, ":")
312324
}
313325
JsonOperator::HashArrow => {
314326
write!(f, "#>")
315327
}
316328
JsonOperator::HashLongArrow => {
317329
write!(f, "#>>")
318330
}
319-
JsonOperator::Colon => {
320-
write!(f, ":")
331+
JsonOperator::HashMinus => write!(f, "#-"),
332+
JsonOperator::LongArrow => {
333+
write!(f, "->>")
321334
}
322-
JsonOperator::AtArrow => {
323-
write!(f, "@>")
335+
JsonOperator::Question => {
336+
write!(f, "?")
324337
}
325-
JsonOperator::ArrowAt => write!(f, "<@"),
326-
JsonOperator::HashMinus => write!(f, "#-"),
327-
JsonOperator::AtQuestion => write!(f, "@?"),
328-
JsonOperator::AtAt => write!(f, "@@"),
338+
JsonOperator::QuestionAnd => write!(f, "?&"),
339+
JsonOperator::QuestionPipe => write!(f, "?|"),
329340
}
330341
}
331342
}

src/parser/mod.rs

+20-11
Original file line numberDiff line numberDiff line change
@@ -2524,25 +2524,31 @@ impl<'a> Parser<'a> {
25242524
right: Box::new(Expr::Value(self.parse_value()?)),
25252525
})
25262526
} else if Token::Arrow == tok
2527-
|| Token::LongArrow == tok
2527+
|| Token::ArrowAt == tok
2528+
|| Token::AtArrow == tok
2529+
|| Token::AtAt == tok
2530+
|| Token::AtQuestion == tok
25282531
|| Token::HashArrow == tok
25292532
|| Token::HashLongArrow == tok
2530-
|| Token::AtArrow == tok
2531-
|| Token::ArrowAt == tok
25322533
|| Token::HashMinus == tok
2533-
|| Token::AtQuestion == tok
2534-
|| Token::AtAt == tok
2534+
|| Token::LongArrow == tok
2535+
|| Token::Question == tok
2536+
|| Token::QuestionAnd == tok
2537+
|| Token::QuestionPipe == tok
25352538
{
25362539
let operator = match tok.token {
25372540
Token::Arrow => JsonOperator::Arrow,
2538-
Token::LongArrow => JsonOperator::LongArrow,
2541+
Token::ArrowAt => JsonOperator::ArrowAt,
2542+
Token::AtArrow => JsonOperator::AtArrow,
2543+
Token::AtAt => JsonOperator::AtAt,
2544+
Token::AtQuestion => JsonOperator::AtQuestion,
25392545
Token::HashArrow => JsonOperator::HashArrow,
25402546
Token::HashLongArrow => JsonOperator::HashLongArrow,
2541-
Token::AtArrow => JsonOperator::AtArrow,
2542-
Token::ArrowAt => JsonOperator::ArrowAt,
25432547
Token::HashMinus => JsonOperator::HashMinus,
2544-
Token::AtQuestion => JsonOperator::AtQuestion,
2545-
Token::AtAt => JsonOperator::AtAt,
2548+
Token::LongArrow => JsonOperator::LongArrow,
2549+
Token::Question => JsonOperator::Question,
2550+
Token::QuestionAnd => JsonOperator::QuestionAnd,
2551+
Token::QuestionPipe => JsonOperator::QuestionPipe,
25462552
_ => unreachable!(),
25472553
};
25482554
Ok(Expr::JsonAccess {
@@ -2788,7 +2794,10 @@ impl<'a> Parser<'a> {
27882794
| Token::ArrowAt
27892795
| Token::HashMinus
27902796
| Token::AtQuestion
2791-
| Token::AtAt => Ok(50),
2797+
| Token::AtAt
2798+
| Token::Question
2799+
| Token::QuestionAnd
2800+
| Token::QuestionPipe => Ok(50),
27922801
_ => Ok(0),
27932802
}
27942803
}

src/tokenizer.rs

+23-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ use sqlparser_derive::{Visit, VisitMut};
3636

3737
use crate::ast::DollarQuotedString;
3838
use crate::dialect::{
39-
BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, SnowflakeDialect,
39+
BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, PostgreSqlDialect,
40+
SnowflakeDialect,
4041
};
4142
use crate::dialect::{Dialect, MySqlDialect};
4243
use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX};
@@ -199,6 +200,15 @@ pub enum Token {
199200
/// for the specified JSON value. Only the first item of the result is taken into
200201
/// account. If the result is not Boolean, then NULL is returned.
201202
AtAt,
203+
/// jsonb ? text -> boolean: Checks whether the string exists as a top-level key within the
204+
/// jsonb object
205+
Question,
206+
/// jsonb ?& text[] -> boolean: Check whether all members of the text array exist as top-level
207+
/// keys within the jsonb object
208+
QuestionAnd,
209+
/// jsonb ?| text[] -> boolean: Check whether any member of the text array exists as top-level
210+
/// keys within the jsonb object
211+
QuestionPipe,
202212
}
203213

204214
impl fmt::Display for Token {
@@ -278,6 +288,9 @@ impl fmt::Display for Token {
278288
Token::HashMinus => write!(f, "#-"),
279289
Token::AtQuestion => write!(f, "@?"),
280290
Token::AtAt => write!(f, "@@"),
291+
Token::Question => write!(f, "?"),
292+
Token::QuestionAnd => write!(f, "?&"),
293+
Token::QuestionPipe => write!(f, "?|"),
281294
}
282295
}
283296
}
@@ -1059,6 +1072,15 @@ impl<'a> Tokenizer<'a> {
10591072
_ => Ok(Some(Token::AtSign)),
10601073
}
10611074
}
1075+
// Postgres uses ? for jsonb operators, not prepared statements
1076+
'?' if dialect_of!(self is PostgreSqlDialect) => {
1077+
chars.next();
1078+
match chars.peek() {
1079+
Some('|') => self.consume_and_return(chars, Token::QuestionPipe),
1080+
Some('&') => self.consume_and_return(chars, Token::QuestionAnd),
1081+
_ => self.consume_and_return(chars, Token::Question),
1082+
}
1083+
}
10621084
'?' => {
10631085
chars.next();
10641086
let s = peeking_take_while(chars, |ch| ch.is_numeric());

tests/sqlparser_common.rs

+26-11
Original file line numberDiff line numberDiff line change
@@ -7655,17 +7655,6 @@ fn test_lock_nonblock() {
76557655

76567656
#[test]
76577657
fn test_placeholder() {
7658-
let sql = "SELECT * FROM student WHERE id = ?";
7659-
let ast = verified_only_select(sql);
7660-
assert_eq!(
7661-
ast.selection,
7662-
Some(Expr::BinaryOp {
7663-
left: Box::new(Expr::Identifier(Ident::new("id"))),
7664-
op: BinaryOperator::Eq,
7665-
right: Box::new(Expr::Value(Value::Placeholder("?".into()))),
7666-
})
7667-
);
7668-
76697658
let dialects = TestedDialects {
76707659
dialects: vec![
76717660
Box::new(GenericDialect {}),
@@ -7705,6 +7694,32 @@ fn test_placeholder() {
77057694
}),
77067695
);
77077696

7697+
let dialects = TestedDialects {
7698+
dialects: vec![
7699+
Box::new(GenericDialect {}),
7700+
Box::new(DuckDbDialect {}),
7701+
// Note: `?` is for jsonb operators in PostgreSqlDialect
7702+
// Box::new(PostgreSqlDialect {}),
7703+
Box::new(MsSqlDialect {}),
7704+
Box::new(AnsiDialect {}),
7705+
Box::new(BigQueryDialect {}),
7706+
Box::new(SnowflakeDialect {}),
7707+
// Note: `$` is the starting word for the HiveDialect identifier
7708+
// Box::new(sqlparser::dialect::HiveDialect {}),
7709+
],
7710+
options: None,
7711+
};
7712+
let sql = "SELECT * FROM student WHERE id = ?";
7713+
let ast = dialects.verified_only_select(sql);
7714+
assert_eq!(
7715+
ast.selection,
7716+
Some(Expr::BinaryOp {
7717+
left: Box::new(Expr::Identifier(Ident::new("id"))),
7718+
op: BinaryOperator::Eq,
7719+
right: Box::new(Expr::Value(Value::Placeholder("?".into()))),
7720+
})
7721+
);
7722+
77087723
let sql = "SELECT $fromage_français, :x, ?123";
77097724
let ast = dialects.verified_only_select(sql);
77107725
assert_eq!(

tests/sqlparser_postgres.rs

+45
Original file line numberDiff line numberDiff line change
@@ -2358,6 +2358,51 @@ fn test_json() {
23582358
},
23592359
select.selection.unwrap(),
23602360
);
2361+
2362+
let sql = r#"SELECT info FROM orders WHERE info ? 'b'"#;
2363+
let select = pg().verified_only_select(sql);
2364+
assert_eq!(
2365+
Expr::JsonAccess {
2366+
left: Box::new(Expr::Identifier(Ident::new("info"))),
2367+
operator: JsonOperator::Question,
2368+
right: Box::new(Expr::Value(Value::SingleQuotedString("b".to_string()))),
2369+
},
2370+
select.selection.unwrap(),
2371+
);
2372+
2373+
let sql = r#"SELECT info FROM orders WHERE info ?& ARRAY['b', 'c']"#;
2374+
let select = pg().verified_only_select(sql);
2375+
assert_eq!(
2376+
Expr::JsonAccess {
2377+
left: Box::new(Expr::Identifier(Ident::new("info"))),
2378+
operator: JsonOperator::QuestionAnd,
2379+
right: Box::new(Expr::Array(Array {
2380+
elem: vec![
2381+
Expr::Value(Value::SingleQuotedString("b".to_string())),
2382+
Expr::Value(Value::SingleQuotedString("c".to_string()))
2383+
],
2384+
named: true
2385+
}))
2386+
},
2387+
select.selection.unwrap(),
2388+
);
2389+
2390+
let sql = r#"SELECT info FROM orders WHERE info ?| ARRAY['b', 'c']"#;
2391+
let select = pg().verified_only_select(sql);
2392+
assert_eq!(
2393+
Expr::JsonAccess {
2394+
left: Box::new(Expr::Identifier(Ident::new("info"))),
2395+
operator: JsonOperator::QuestionPipe,
2396+
right: Box::new(Expr::Array(Array {
2397+
elem: vec![
2398+
Expr::Value(Value::SingleQuotedString("b".to_string())),
2399+
Expr::Value(Value::SingleQuotedString("c".to_string()))
2400+
],
2401+
named: true
2402+
}))
2403+
},
2404+
select.selection.unwrap(),
2405+
);
23612406
}
23622407

23632408
#[test]

0 commit comments

Comments
 (0)