Skip to content
Closed
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
17 changes: 17 additions & 0 deletions src/dialect/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@
// specific language governing permissions and limitations
// under the License.

use crate::dialect::mysql::JSON_EXTRACTION_PREC;
use crate::dialect::Dialect;
use crate::parser::{Parser, ParserError};
use crate::tokenizer::Token;

/// A permissive, general purpose [`Dialect`], which parses a wide variety of SQL
/// statements, from many different dialects.
Expand All @@ -24,6 +27,20 @@ use crate::dialect::Dialect;
pub struct GenericDialect;

impl Dialect for GenericDialect {
/// `->` and `->>` follow MySQL here rather than PostgreSQL.
///
/// The two engines disagree: PostgreSQL treats the arrows as ordinary
/// operators below arithmetic, while in MySQL `c -> '$.a'` is
/// `JSON_EXTRACT(c, '$.a')` and binds before it. A permissive dialect has
/// to pick one, and the contributing guide asks that a dialect-specific
/// feature parse under both that dialect and this one.
fn get_next_precedence(&self, parser: &Parser) -> Option<Result<u8, ParserError>> {
match parser.peek_token_ref().token {
Token::Arrow | Token::LongArrow => Some(Ok(JSON_EXTRACTION_PREC)),
_ => None,
}
}

fn supports_map_typedef_with_parentheses(&self) -> bool {
true
}
Expand Down
18 changes: 15 additions & 3 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -980,9 +980,14 @@ pub trait Dialect: Debug + Any {
// string columns. See `JsonAccess`.
_ => Ok(p!(Colon)),
},
Token::Arrow
| Token::LongArrow
| Token::HashArrow
// `->` and `->>` are separated from the rest of the family
// because dialects disagree about them: in PostgreSQL they are
// ordinary operators, while in MySQL `c -> '$.a'` means
// `JSON_EXTRACT(c, '$.a')` and has to bind before arithmetic. The
// default value is the same for both, and the dialects that need
// the MySQL grouping override these two tokens.
Token::Arrow | Token::LongArrow => Ok(p!(JsonExtraction)),
Token::HashArrow
| Token::HashLongArrow
| Token::AtArrow
| Token::ArrowAt
Expand Down Expand Up @@ -1033,6 +1038,11 @@ pub trait Dialect: Debug + Any {
Precedence::Ampersand => 23,
Precedence::Caret => 22,
Precedence::Pipe => 21,
// Same value as `PgOther`, which is where the arrows have always
// sat. The variant exists so that a dialect whose arrows extract
// from JSON can move them without moving the rest of that family;
// see `MySqlDialect::get_next_precedence`.
Precedence::JsonExtraction => 21,
Precedence::Colon => 21,
// "any other operator" -- `->`, `@>`, custom operators. PostgreSQL
// places this row above `BETWEEN` / `LIKE` and below `+` / `-`
Expand Down Expand Up @@ -1991,6 +2001,8 @@ pub enum Precedence {
Pipe,
/// `:` operator for json/variant access.
Colon,
/// `->` and `->>`, which extract from JSON and bind before arithmetic.
JsonExtraction,
/// `BETWEEN` operator.
Between,
/// Equality operator (`=`).
Expand Down
17 changes: 17 additions & 0 deletions src/dialect/mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use crate::{
dialect::Dialect,
keywords::Keyword,
parser::{Parser, ParserError},
tokenizer::Token,
};

use super::keywords;
Expand All @@ -39,6 +40,13 @@ const RESERVED_FOR_TABLE_ALIAS_MYSQL: &[Keyword] = &[
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MySqlDialect {}

/// Precedence of `->` and `->>` in MySQL.
///
/// `c -> '$.a'` is defined as `JSON_EXTRACT(c, '$.a')`, so the extraction has
/// to happen before any operator can use its result. That puts it above
/// `MulDivModOp` (40) and `AtTz` (41), and below `DoubleColon` (50).
pub(crate) const JSON_EXTRACTION_PREC: u8 = 42;

impl Dialect for MySqlDialect {
fn is_identifier_start(&self, ch: char) -> bool {
// See https://dev.mysql.com/doc/refman/8.0/en/identifiers.html.
Expand All @@ -59,6 +67,15 @@ impl Dialect for MySqlDialect {
!ch.is_ascii()
}

fn get_next_precedence(&self, parser: &Parser) -> Option<Result<u8, ParserError>> {
match parser.peek_token_ref().token {
// The rest of the `PgOther` family keeps the shared value: only
// these two extract from JSON in MySQL.
Token::Arrow | Token::LongArrow => Some(Ok(JSON_EXTRACTION_PREC)),
_ => None,
}
}

fn is_delimited_identifier_start(&self, ch: char) -> bool {
ch == '`'
}
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/postgresql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ impl Dialect for PostgreSqlDialect {
Precedence::Eq => EQ_PREC,
Precedence::Like => BETWEEN_LIKE_PREC,
Precedence::Is => IS_PREC,
// PostgreSQL's `->` is an ordinary operator and belongs below
// arithmetic, which is where it already sits. The dedicated
// variant exists for dialects whose arrows extract from JSON.
Precedence::JsonExtraction => PG_OTHER_PREC,
Precedence::PgOther => PG_OTHER_PREC,
Precedence::UnaryNot => NOT_PREC,
Precedence::And => AND_PREC,
Expand Down
92 changes: 92 additions & 0 deletions tests/sqlparser_mysql.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5053,6 +5053,98 @@ fn parse_is_distinct_from_json_arrow_precedence() {
);
}

#[test]
fn parse_json_arrow_arithmetic_precedence() {
let col = || Expr::Identifier(Ident::new("c"));
let path = || Expr::Value(Value::SingleQuotedString("$.a".into()).with_empty_span());
let extract = |op: BinaryOperator| Expr::BinaryOp {
left: Box::new(col()),
op,
right: Box::new(path()),
};
let num = |n: &str| Expr::value(number(n));
let binop = |left: Expr, op: BinaryOperator, right: Expr| Expr::BinaryOp {
left: Box::new(left),
op,
right: Box::new(right),
};

// `c -> path` is defined as `JSON_EXTRACT(c, path)`, so the extraction has
// to happen before any arithmetic can use its result: `c -> '$.a' + 1` is
// `(c -> '$.a') + 1` rather than `c -> ('$.a' + 1)`, which would add a
// number to a JSON path.
assert_eq!(
binop(
extract(BinaryOperator::Arrow),
BinaryOperator::Plus,
num("1")
),
mysql_and_generic().verified_expr("c -> '$.a' + 1")
);
assert_eq!(
binop(
extract(BinaryOperator::LongArrow),
BinaryOperator::Plus,
num("1")
),
mysql_and_generic().verified_expr("c ->> '$.a' + 1")
);
assert_eq!(
binop(
extract(BinaryOperator::Arrow),
BinaryOperator::Multiply,
num("2")
),
mysql_and_generic().verified_expr("c -> '$.a' * 2")
);

// The same on the other side: the arrow takes `c`, not `1 + c`.
assert_eq!(
binop(
num("1"),
BinaryOperator::Plus,
extract(BinaryOperator::Arrow)
),
mysql_and_generic().verified_expr("1 + c -> '$.a'")
);

// Bitwise operators bind less tightly than the arrow too, which the
// arithmetic cases above do not cover: `&` and `^` sit above `->` in the
// precedence table while MySQL puts them below it.
assert_eq!(
binop(
extract(BinaryOperator::Arrow),
BinaryOperator::BitwiseAnd,
num("1")
),
mysql_and_generic().verified_expr("c -> '$.a' & 1")
);
assert_eq!(
binop(
extract(BinaryOperator::Arrow),
BinaryOperator::BitwiseXor,
num("1")
),
mysql_and_generic().verified_expr("c -> '$.a' ^ 1")
);

// These two already group correctly and are kept so that a change to the
// arrow's precedence cannot break them: `=` sits below the arrow, and `|`
// shares its level, where left associativity gives the same answer.
assert_eq!(
binop(extract(BinaryOperator::Arrow), BinaryOperator::Eq, num("1")),
mysql_and_generic().verified_expr("c -> '$.a' = 1")
);
assert_eq!(
binop(
extract(BinaryOperator::Arrow),
BinaryOperator::BitwiseOr,
num("1")
),
mysql_and_generic().verified_expr("c -> '$.a' | 1")
);
}

#[test]
fn parse_bitstring_literal_escaping() {
mysql_and_generic().verified_stmt("SELECT B''''");
Expand Down
Loading