Skip to content
1 change: 1 addition & 0 deletions fuzz/fuzz_seeds/sqlite_2470_is_json_function.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT a IS json('{}') FROM t
1 change: 1 addition & 0 deletions fuzz/fuzz_seeds/sqlite_2470_is_operators.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SELECT a IS b, a IS NOT b, a IS b + c, a IS b AND c IS NOT d, a IS b = c, a IS b IN (1) FROM t
6 changes: 6 additions & 0 deletions src/ast/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,10 @@ pub enum BinaryOperator {
/// ':=' Assignment Operator
/// See <https://dev.mysql.com/doc/refman/8.4/en/assignment-operators.html#operator_assign-value>
Assignment,
/// `IS` operator
Is,
/// `IS NOT` operator
IsNot,
}

impl fmt::Display for BinaryOperator {
Expand Down Expand Up @@ -409,6 +413,8 @@ impl fmt::Display for BinaryOperator {
BinaryOperator::At => f.write_str("@"),
BinaryOperator::TildeEq => f.write_str("~="),
BinaryOperator::Assignment => f.write_str(":="),
BinaryOperator::Is => f.write_str("IS"),
BinaryOperator::IsNot => f.write_str("IS NOT"),
}
}
}
8 changes: 8 additions & 0 deletions src/dialect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,14 @@ pub trait Dialect: Debug + Any {
false
}

/// Returns true if the dialect supports binary `IS` and `IS NOT` operators.
///
/// Their right operand is any expression, so `IS UNKNOWN`, `IS JSON` and
/// `IS NORMALIZED` are not parsed as predicates.
fn supports_is_operator(&self) -> bool {
false
}

/// Returns true if this dialect allows an optional `SIGNED` suffix after integer data types.
///
/// Example:
Expand Down
4 changes: 4 additions & 0 deletions src/dialect/sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ impl Dialect for SQLiteDialect {
true
}

fn supports_is_operator(&self) -> bool {
true
}

fn supports_comma_separated_trim(&self) -> bool {
true
}
Expand Down
22 changes: 18 additions & 4 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4194,10 +4194,6 @@ impl<'a> Parser<'a> {
Ok(Expr::IsFalse(Box::new(expr)))
} else if self.parse_keywords(&[Keyword::NOT, Keyword::FALSE]) {
Ok(Expr::IsNotFalse(Box::new(expr)))
} else if self.parse_keywords(&[Keyword::UNKNOWN]) {
Ok(Expr::IsUnknown(Box::new(expr)))
} else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) {
Ok(Expr::IsNotUnknown(Box::new(expr)))
} else if self.parse_keywords(&[Keyword::DISTINCT, Keyword::FROM]) {
// The right operand binds no more loosely than `IS`
// itself, so that e.g. `a IS DISTINCT FROM b AND c`
Expand All @@ -4208,6 +4204,24 @@ impl<'a> Parser<'a> {
{
let expr2 = self.parse_subexpr(precedence)?;
Ok(Expr::IsNotDistinctFrom(Box::new(expr), Box::new(expr2)))
} else if self.dialect.supports_is_operator() {
let op = if self.parse_keyword(Keyword::NOT) {
BinaryOperator::IsNot
} else {
BinaryOperator::Is
};
// SQLite gives `IS` the same precedence as `=` (https://www.sqlite.org/lang_expr.html).
Ok(Expr::BinaryOp {
left: Box::new(expr),
op,
right: Box::new(
self.parse_subexpr(self.dialect.prec_value(Precedence::Eq))?,
),
})
} else if self.parse_keywords(&[Keyword::UNKNOWN]) {
Ok(Expr::IsUnknown(Box::new(expr)))
} else if self.parse_keywords(&[Keyword::NOT, Keyword::UNKNOWN]) {
Ok(Expr::IsNotUnknown(Box::new(expr)))
} else if self.parse_keyword(Keyword::JSON) {
self.parse_is_json_predicate(expr, false)
} else if self.parse_keywords(&[Keyword::NOT, Keyword::JSON]) {
Expand Down
59 changes: 31 additions & 28 deletions tests/sqlparser_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11746,14 +11746,16 @@ fn parse_is_boolean() {
verified_expr(sql)
);

let dialects = all_dialects_except(|dialect| dialect.supports_is_operator());

let sql = "a IS NORMALIZED";
assert_eq!(
IsNormalized {
expr: Box::new(Identifier(Ident::new("a"))),
form: None,
negated: false,
},
verified_expr(sql)
dialects.verified_expr(sql)
);

let sql = "a IS NOT NORMALIZED";
Expand All @@ -11763,7 +11765,7 @@ fn parse_is_boolean() {
form: None,
negated: true,
},
verified_expr(sql)
dialects.verified_expr(sql)
);

let sql = "a IS NFKC NORMALIZED";
Expand All @@ -11773,7 +11775,7 @@ fn parse_is_boolean() {
form: Some(NormalizationForm::NFKC),
negated: false,
},
verified_expr(sql)
dialects.verified_expr(sql)
);

let sql = "a IS NOT NFKD NORMALIZED";
Expand All @@ -11783,19 +11785,19 @@ fn parse_is_boolean() {
form: Some(NormalizationForm::NFKD),
negated: true,
},
verified_expr(sql)
dialects.verified_expr(sql)
);

let sql = "a IS UNKNOWN";
assert_eq!(
IsUnknown(Box::new(Identifier(Ident::new("a")))),
verified_expr(sql)
dialects.verified_expr(sql)
);

let sql = "a IS NOT UNKNOWN";
assert_eq!(
IsNotUnknown(Box::new(Identifier(Ident::new("a")))),
verified_expr(sql)
dialects.verified_expr(sql)
);

verified_stmt("SELECT f FROM foo WHERE field IS TRUE");
Expand All @@ -11804,14 +11806,14 @@ fn parse_is_boolean() {
verified_stmt("SELECT f FROM foo WHERE field IS FALSE");
verified_stmt("SELECT f FROM foo WHERE field IS NOT FALSE");

verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED");
verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED");
verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED");
verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED");
verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NORMALIZED");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NFC NORMALIZED");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NFD NORMALIZED");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT NORMALIZED");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT NFKC NORMALIZED");

verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN");
verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS UNKNOWN");
dialects.verified_stmt("SELECT f FROM foo WHERE field IS NOT UNKNOWN");

for (sql, found) in [
("SELECT f from foo where field is 0", "0"),
Expand All @@ -11820,7 +11822,7 @@ fn parse_is_boolean() {
("SELECT s, s IS TRIM(' NFKC ') FROM foo", "TRIM"),
] {
assert_eq!(
parse_sql_statements(sql),
dialects.parse_sql_statements(sql),
Err(ParserError::ParserError(format!(
"Expected: [NOT] NULL | TRUE | FALSE | DISTINCT | [NOT] JSON [VALUE | SCALAR | ARRAY | OBJECT] [WITH | WITHOUT UNIQUE [KEYS]] | [form] NORMALIZED FROM after IS, found: {found}"
))),
Expand All @@ -11832,6 +11834,7 @@ fn parse_is_boolean() {
#[test]
fn parse_is_json_predicate() {
use self::Expr::*;
let dialects = all_dialects_except(|dialect| dialect.supports_is_operator());

// Assert the full AST once for a case that exercises every field.
let sql = "a IS NOT JSON OBJECT WITHOUT UNIQUE KEYS";
Expand All @@ -11842,24 +11845,24 @@ fn parse_is_json_predicate() {
unique_keys: Some(JsonKeyUniqueness::WithoutUniqueKeys),
negated: true,
},
verified_expr(sql)
dialects.verified_expr(sql)
);

// The remaining forms only need to round-trip.
verified_expr("a IS JSON");
verified_expr("a IS NOT JSON");
verified_expr("a IS JSON VALUE");
verified_expr("a IS JSON SCALAR");
verified_expr("a IS JSON ARRAY");
verified_expr("a IS JSON OBJECT");
verified_expr("a IS JSON WITH UNIQUE KEYS");
verified_expr("a IS JSON WITHOUT UNIQUE KEYS");

all_dialects().expr_parses_to("a IS JSON WITH UNIQUE", "a IS JSON WITH UNIQUE KEYS");
all_dialects().expr_parses_to("a IS JSON WITHOUT UNIQUE", "a IS JSON WITHOUT UNIQUE KEYS");
dialects.verified_expr("a IS JSON");
dialects.verified_expr("a IS NOT JSON");
dialects.verified_expr("a IS JSON VALUE");
dialects.verified_expr("a IS JSON SCALAR");
dialects.verified_expr("a IS JSON ARRAY");
dialects.verified_expr("a IS JSON OBJECT");
dialects.verified_expr("a IS JSON WITH UNIQUE KEYS");
dialects.verified_expr("a IS JSON WITHOUT UNIQUE KEYS");

dialects.expr_parses_to("a IS JSON WITH UNIQUE", "a IS JSON WITH UNIQUE KEYS");
dialects.expr_parses_to("a IS JSON WITHOUT UNIQUE", "a IS JSON WITHOUT UNIQUE KEYS");

assert_matches!(
verified_expr("NOT a IS JSON"),
dialects.verified_expr("NOT a IS JSON"),
Expr::UnaryOp {
op: UnaryOperator::Not,
expr
Expand All @@ -11869,7 +11872,7 @@ fn parse_is_json_predicate() {

#[test]
fn parse_is_json_predicate_invalid() {
let dialects = all_dialects();
let dialects = all_dialects_except(|dialect| dialect.supports_is_operator());

for (sql, err) in [
(
Expand Down
132 changes: 132 additions & 0 deletions tests/sqlparser_sqlite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,138 @@ fn test_glob_operator() {
);
}

#[test]
fn sqlite_is_operators() {
for (sql, op) in [
("a IS b", BinaryOperator::Is),
("a IS NOT b", BinaryOperator::IsNot),
] {
assert_eq!(
sqlite().verified_expr(sql),
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op,
right: Box::new(Expr::Identifier(Ident::new("b"))),
}
);
}

assert_eq!(
sqlite().verified_expr("a IS b + c"),
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op: BinaryOperator::Is,
right: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("b"))),
op: BinaryOperator::Plus,
right: Box::new(Expr::Identifier(Ident::new("c"))),
}),
}
);

assert_eq!(
sqlite().verified_expr("a IS b AND c IS NOT d"),
Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op: BinaryOperator::Is,
right: Box::new(Expr::Identifier(Ident::new("b"))),
}),
op: BinaryOperator::And,
right: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("c"))),
op: BinaryOperator::IsNot,
right: Box::new(Expr::Identifier(Ident::new("d"))),
}),
}
);

assert_eq!(
sqlite().verified_expr("a IS b = c"),
Expr::BinaryOp {
left: Box::new(Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op: BinaryOperator::Is,
right: Box::new(Expr::Identifier(Ident::new("b"))),
}),
op: BinaryOperator::Eq,
right: Box::new(Expr::Identifier(Ident::new("c"))),
}
);

match sqlite().verified_expr("a IS b IN (1)") {
Expr::In {
expr,
target: InTarget::List(list),
negated,
global,
} => {
assert!(!negated);
assert!(!global);
assert_eq!(
*expr,
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op: BinaryOperator::Is,
right: Box::new(Expr::Identifier(Ident::new("b"))),
}
);
assert_eq!(list.len(), 1);
assert!(matches!(
list[0],
Expr::Value(ValueWithSpan {
value: Value::Number(_, _),
..
})
));
}
other => panic!("expected InList on top, got {other:?}"),
}

// A dangling right-hand side is rejected
for sql in ["SELECT a IS", "SELECT a IS NOT"] {
assert_eq!(
sqlite().parse_sql_statements(sql),
Err(ParserError::ParserError(
"Expected: an expression, found: EOF".to_string()
))
);
}
}

#[test]
fn sqlite_is_operator_keyword_operands() {
// SQLite has no `IS UNKNOWN`, `IS JSON` or `IS NORMALIZED` predicate.
for right in ["json('{}')", "json", "unknown", "normalized", "nfc"] {
for (op, not) in [(BinaryOperator::Is, ""), (BinaryOperator::IsNot, "NOT ")] {
let sql = format!("a IS {not}{right}");
assert_eq!(
sqlite().verified_expr(&sql),
Expr::BinaryOp {
left: Box::new(Expr::Identifier(Ident::new("a"))),
op,
right: Box::new(sqlite().verified_expr(right)),
},
"{sql}"
);
}
}
sqlite().verified_stmt("SELECT a IS json('{}'), a IS NOT json('{}') FROM t");

for (sql, found) in [
("SELECT a FROM t WHERE a IS NFC NORMALIZED", "NORMALIZED"),
("SELECT a FROM t WHERE a IS NOT JSON OBJECT", "OBJECT"),
] {
assert_eq!(
sqlite().parse_sql_statements(sql),
Err(ParserError::ParserError(format!(
"Expected: end of statement, found: {found}"
))),
"{sql}"
);
}
}

#[test]
fn test_update_delete_limit() {
match sqlite().verified_stmt("UPDATE foo SET bar = 1 LIMIT 99") {
Expand Down
Loading