Skip to content

Support ?-based jsonb operators in Postgres #1242

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/ast/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,27 @@ pub enum BinaryOperator {
///
/// See <https://www.postgresql.org/docs/current/functions-json.html>.
AtQuestion,
/// The `?` operator.
///
/// On PostgreSQL, this operator is used to check whether a string exists as a top-level key
/// within the JSON value
///
/// See <https://www.postgresql.org/docs/current/functions-json.html>.
Question,
/// The `?&` operator.
///
/// On PostgreSQL, this operator is used to check whether all of the the indicated array
/// members exist as top-level keys.
///
/// See <https://www.postgresql.org/docs/current/functions-json.html>.
QuestionAnd,
/// The `?|` operator.
///
/// On PostgreSQL, this operator is used to check whether any of the the indicated array
/// members exist as top-level keys.
///
/// See <https://www.postgresql.org/docs/current/functions-json.html>.
QuestionPipe,
/// PostgreSQL-specific custom operator.
///
/// See [CREATE OPERATOR](https://www.postgresql.org/docs/current/sql-createoperator.html)
Expand Down Expand Up @@ -269,6 +290,9 @@ impl fmt::Display for BinaryOperator {
BinaryOperator::ArrowAt => f.write_str("<@"),
BinaryOperator::HashMinus => f.write_str("#-"),
BinaryOperator::AtQuestion => f.write_str("@?"),
BinaryOperator::Question => f.write_str("?"),
BinaryOperator::QuestionAnd => f.write_str("?&"),
BinaryOperator::QuestionPipe => f.write_str("?|"),
BinaryOperator::PGCustomBinaryOperator(idents) => {
write!(f, "OPERATOR({})", display_separated(idents, "."))
}
Expand Down
8 changes: 7 additions & 1 deletion src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2355,6 +2355,9 @@ impl<'a> Parser<'a> {
Token::HashMinus => Some(BinaryOperator::HashMinus),
Token::AtQuestion => Some(BinaryOperator::AtQuestion),
Token::AtAt => Some(BinaryOperator::AtAt),
Token::Question => Some(BinaryOperator::Question),
Token::QuestionAnd => Some(BinaryOperator::QuestionAnd),
Token::QuestionPipe => Some(BinaryOperator::QuestionPipe),

Token::Word(w) => match w.keyword {
Keyword::AND => Some(BinaryOperator::And),
Expand Down Expand Up @@ -2851,7 +2854,10 @@ impl<'a> Parser<'a> {
| Token::ArrowAt
| Token::HashMinus
| Token::AtQuestion
| Token::AtAt => Ok(Self::PG_OTHER_PREC),
| Token::AtAt
| Token::Question
| Token::QuestionAnd
| Token::QuestionPipe => Ok(Self::PG_OTHER_PREC),
_ => Ok(0),
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ use sqlparser_derive::{Visit, VisitMut};

use crate::ast::DollarQuotedString;
use crate::dialect::{
BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, SnowflakeDialect,
BigQueryDialect, DuckDbDialect, GenericDialect, HiveDialect, PostgreSqlDialect,
SnowflakeDialect,
};
use crate::dialect::{Dialect, MySqlDialect};
use crate::keywords::{Keyword, ALL_KEYWORDS, ALL_KEYWORDS_INDEX};
Expand Down Expand Up @@ -199,6 +200,15 @@ pub enum Token {
/// for the specified JSON value. Only the first item of the result is taken into
/// account. If the result is not Boolean, then NULL is returned.
AtAt,
/// jsonb ? text -> boolean: Checks whether the string exists as a top-level key within the
/// jsonb object
Question,
/// jsonb ?& text[] -> boolean: Check whether all members of the text array exist as top-level
/// keys within the jsonb object
QuestionAnd,
/// jsonb ?| text[] -> boolean: Check whether any member of the text array exists as top-level
/// keys within the jsonb object
QuestionPipe,
}

impl fmt::Display for Token {
Expand Down Expand Up @@ -278,6 +288,9 @@ impl fmt::Display for Token {
Token::HashMinus => write!(f, "#-"),
Token::AtQuestion => write!(f, "@?"),
Token::AtAt => write!(f, "@@"),
Token::Question => write!(f, "?"),
Token::QuestionAnd => write!(f, "?&"),
Token::QuestionPipe => write!(f, "?|"),
}
}
}
Expand Down Expand Up @@ -1059,6 +1072,15 @@ impl<'a> Tokenizer<'a> {
_ => Ok(Some(Token::AtSign)),
}
}
// Postgres uses ? for jsonb operators, not prepared statements
'?' if dialect_of!(self is PostgreSqlDialect) => {
chars.next();
match chars.peek() {
Some('|') => self.consume_and_return(chars, Token::QuestionPipe),
Some('&') => self.consume_and_return(chars, Token::QuestionAnd),
_ => self.consume_and_return(chars, Token::Question),
}
}
'?' => {
chars.next();
let s = peeking_take_while(chars, |ch| ch.is_numeric());
Expand Down
37 changes: 26 additions & 11 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7750,17 +7750,6 @@ fn test_lock_nonblock() {

#[test]
fn test_placeholder() {
let sql = "SELECT * FROM student WHERE id = ?";
let ast = verified_only_select(sql);
assert_eq!(
ast.selection,
Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("id"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(Value::Placeholder("?".into()))),
})
);

let dialects = TestedDialects {
dialects: vec![
Box::new(GenericDialect {}),
Expand Down Expand Up @@ -7800,6 +7789,32 @@ fn test_placeholder() {
}),
);

let dialects = TestedDialects {
dialects: vec![
Box::new(GenericDialect {}),
Box::new(DuckDbDialect {}),
// Note: `?` is for jsonb operators in PostgreSqlDialect
// Box::new(PostgreSqlDialect {}),
Box::new(MsSqlDialect {}),
Box::new(AnsiDialect {}),
Box::new(BigQueryDialect {}),
Box::new(SnowflakeDialect {}),
// Note: `$` is the starting word for the HiveDialect identifier
// Box::new(sqlparser::dialect::HiveDialect {}),
],
options: None,
};
let sql = "SELECT * FROM student WHERE id = ?";
let ast = dialects.verified_only_select(sql);
assert_eq!(
ast.selection,
Some(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("id"))),
op: BinaryOperator::Eq,
right: Box::new(Expr::Value(Value::Placeholder("?".into()))),
})
);

let sql = "SELECT $fromage_français, :x, ?123";
let ast = dialects.verified_only_select(sql);
assert_eq!(
Expand Down
45 changes: 45 additions & 0 deletions tests/sqlparser_postgres.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,51 @@ fn test_json() {
},
select.selection.unwrap(),
);

let sql = r#"SELECT info FROM orders WHERE info ? 'b'"#;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we please add coverage for all new operators that were added?

Perhaps just verifying that they roundtrip ( no need to reverify the parsed structure for all of them), perhaps using one_statement_parses_to or something similar

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alamb The tests at lines 2373 and at 2390 should cover the other of the three new operators added, I believe? Sorry, not sure what I'm missing here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I'm cleaning up the conflicts after getting that refactor merged in. Should be up shortly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And done. Assuming the three added tests hit the operators as needed, should be good to go. Just let me know!

Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup --- looks good to me. Thank you (I think it was unclear to me that only three new operators got added initially). Sorry for my confusion

let select = pg().verified_only_select(sql);
assert_eq!(
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("info"))),
op: BinaryOperator::Question,
right: Box::new(Expr::Value(Value::SingleQuotedString("b".to_string()))),
},
select.selection.unwrap(),
);

let sql = r#"SELECT info FROM orders WHERE info ?& ARRAY['b', 'c']"#;
let select = pg().verified_only_select(sql);
assert_eq!(
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("info"))),
op: BinaryOperator::QuestionAnd,
right: Box::new(Expr::Array(Array {
elem: vec![
Expr::Value(Value::SingleQuotedString("b".to_string())),
Expr::Value(Value::SingleQuotedString("c".to_string()))
],
named: true
}))
},
select.selection.unwrap(),
);

let sql = r#"SELECT info FROM orders WHERE info ?| ARRAY['b', 'c']"#;
let select = pg().verified_only_select(sql);
assert_eq!(
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("info"))),
op: BinaryOperator::QuestionPipe,
right: Box::new(Expr::Array(Array {
elem: vec![
Expr::Value(Value::SingleQuotedString("b".to_string())),
Expr::Value(Value::SingleQuotedString("c".to_string()))
],
named: true
}))
},
select.selection.unwrap(),
);
}

#[test]
Expand Down
Loading