diff --git a/sqlglot/parser.py b/sqlglot/parser.py index 1f5c09e63f..bc6a473176 100644 --- a/sqlglot/parser.py +++ b/sqlglot/parser.py @@ -1208,7 +1208,7 @@ class Parser: UNARY_PARSERS: t.ClassVar = { TokenType.PLUS: lambda self: self._parse_unary(), # Unary + is handled as a no-op - TokenType.NOT: lambda self: self.expression(exp.Not(this=self._parse_equality())), + TokenType.NOT: lambda self: self.expression(exp.Not(this=self._parse_is_predicate())), TokenType.TILDE: lambda self: self.expression(exp.BitwiseNot(this=self._parse_unary())), TokenType.DASH: lambda self: self.expression(exp.Neg(this=self._parse_unary())), TokenType.PIPE_SLASH: lambda self: self.expression(exp.Sqrt(this=self._parse_unary())), @@ -1286,7 +1286,6 @@ class Parser: TokenType.ILIKE: binary_range_parser(exp.ILike), TokenType.IN: lambda self, this: self._parse_in(this), TokenType.IRLIKE: binary_range_parser(exp.RegexpILike), - TokenType.IS: lambda self, this: self._parse_is(this), TokenType.LIKE: binary_range_parser(exp.Like), TokenType.LT_AT: binary_range_parser(exp.ArrayContainedBy), TokenType.OVERLAPS: binary_range_parser(exp.Overlaps), @@ -6177,17 +6176,48 @@ def _parse_disjunction(self) -> exp.Expr | None: return this def _parse_conjunction(self) -> exp.Expr | None: - this = self._parse_equality() + this = self._parse_is_predicate() while self._match_set(self.CONJUNCTION): comments = self._prev_comments this = self.expression( self.CONJUNCTION[self._prev.token_type]( - this=this, expression=self._parse_equality() + this=this, expression=self._parse_is_predicate() ), comments=comments, ) return this + def _parse_is_predicate(self) -> exp.Expr | None: + this = self._parse_equality() + while True: + negate = self._match(TokenType.NOT) + if self._match(TokenType.IS): + expression = self._parse_is(this) + if not expression: + if negate: + self._retreat(self._index - 1) + return this + + this = expression + if negate: + this = self.expression(exp.Not(this=this)) + elif self._match(TokenType.ISNULL) or (negate and self._match(TokenType.NULL)): + this = self.expression(exp.Is(this=this, expression=exp.Null())) + this = self.expression(exp.Not(this=this)) if negate else this + elif self._match(TokenType.NOTNULL): + # Postgres supports ISNULL and NOTNULL for conditions. + # https://blog.andreiavram.ro/postgresql-null-composite-type/ + if self.dialect.NORMALIZE_NOT_NULL: + this = self.expression(exp.Is(this=this, expression=exp.Null())) + this = self.expression(exp.Not(this=this)) + else: + this = self.expression(exp.Is(this=this, expression=exp.Null(), negate=True)) + else: + if negate: + self._retreat(self._index - 1) + break + return this + def _parse_equality(self) -> exp.Expr | None: this = self._parse_comparison() while self._match_set(self.EQUALITY): @@ -6221,16 +6251,6 @@ def _parse_range(self, this: exp.Expr | None = None) -> exp.Expr | None: return this this = expression - elif self._match(TokenType.ISNULL) or (negate and self._match(TokenType.NULL)): - this = self.expression(exp.Is(this=this, expression=exp.Null())) - elif self._match(TokenType.NOTNULL): - # Postgres supports ISNULL and NOTNULL for conditions. - # https://blog.andreiavram.ro/postgresql-null-composite-type/ - if self.dialect.NORMALIZE_NOT_NULL: - this = self.expression(exp.Is(this=this, expression=exp.Null())) - this = self.expression(exp.Not(this=this)) - else: - this = self.expression(exp.Is(this=this, expression=exp.Null(), negate=True)) else: if negate: self._retreat(self._index - 1) @@ -6239,7 +6259,7 @@ def _parse_range(self, this: exp.Expr | None = None) -> exp.Expr | None: if negate: this = self._negate_range(this) if self._curr and ( - self._curr.token_type == TokenType.NOT + self._curr.token_type in (TokenType.NOT, TokenType.IS) or self._curr.token_type in self.RANGE_PARSERS ): this = self.expression(exp.Paren(this=this)) diff --git a/tests/dialects/test_postgres.py b/tests/dialects/test_postgres.py index cb94d67574..c9d69399b1 100644 --- a/tests/dialects/test_postgres.py +++ b/tests/dialects/test_postgres.py @@ -79,6 +79,11 @@ def test_postgres(self): self.validate_identity("SELECT DATE_TRUNC('DAY', x, 'America/New_York')") self.validate_identity("STRING_TO_ARRAY('xx~^~yy~^~zz', '~^~', 'yy')") self.validate_identity("SELECT x FROM t WHERE CAST($1 AS TEXT) = 'ok'") + self.validate_identity("SELECT * FROM t WHERE t.a = t.b IS NULL") + self.validate_identity("SELECT * FROM t WHERE t.a = (t.b IS NULL)") + self.validate_identity("SELECT * FROM t WHERE t.a = t.b IS NOT NULL") + self.validate_identity("SELECT * FROM t WHERE t.a = t.b IS TRUE") + self.validate_identity("SELECT * FROM t WHERE t.a = t.b IS FALSE") self.validate_identity("SELECT * FROM t TABLESAMPLE SYSTEM (50) REPEATABLE (55)") self.validate_identity("x @@ y") self.validate_identity("doc @? '$.a[*] ? (@ > 2)'") diff --git a/tests/test_parser.py b/tests/test_parser.py index 8af71dfd96..09514ff767 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -281,6 +281,21 @@ def test_mod_precedence(self): self.assertEqual(expression.this.expression.sql(), "3") self.assertEqual(expression.expression.sql(), "2") + def test_is_precedence(self): + expression = parse_one("SELECT 1 = 1 IS NULL").expressions[0] + + self.assertIsInstance(expression, exp.Is) + self.assertIsInstance(expression.this, exp.EQ) + self.assertEqual(expression.this.this.sql(), "1") + self.assertEqual(expression.this.expression.sql(), "1") + self.assertIsInstance(expression.expression, exp.Null) + + parenthesized = parse_one("SELECT 1 = (1 IS NULL)").expressions[0] + self.assertIsInstance(parenthesized, exp.EQ) + self.assertIsInstance(parenthesized.expression.this, exp.Is) + + self.assertEqual(parse_one("SELECT 1 is").sql(), "SELECT 1 AS is") + def test_limit_percent_parses_without_modulo(self): expression = parse_one("SELECT * FROM range(1000) LIMIT 1 / 10% OFFSET 5") limit = expression.args["limit"]