Skip to content

Commit ddea61d

Browse files
authored
fix(optimizer)!: simplify connector complements only for non-null operands (#6214)
* fix(optimizer)!: simplify connector complements only for literal and null * remove is_null * refactor impl with nullable * impl with nonnull * update check flags * simplify code * fix type annotation * annotation check * add more tests for nonnull propagation * more tests * tests for unary * more tests * fix annotation call * add meta * fix case of nullable * meta with nonnull * refactor comment * add annotate for column like ch * refactor nonnull
1 parent dba0414 commit ddea61d

File tree

4 files changed

+103
-7
lines changed

4 files changed

+103
-7
lines changed

sqlglot/optimizer/annotate_types.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,9 @@ def annotate_scope(self, scope: Scope) -> None:
314314
elif isinstance(source.expression, exp.Unnest):
315315
self._set_type(col, source.expression.type)
316316

317+
if col.type and col.type.args.get("nullable") is False:
318+
col.meta["nonnull"] = True
319+
317320
if isinstance(self.schema, MappingSchema):
318321
for table_column in scope.table_columns:
319322
source = scope.sources.get(table_column.name)
@@ -446,6 +449,11 @@ def _annotate_binary(self, expression: B) -> B:
446449
else:
447450
self._set_type(expression, self._maybe_coerce(left_type, right_type))
448451

452+
if isinstance(expression, exp.Is) or (
453+
left.meta.get("nonnull") is True and right.meta.get("nonnull") is True
454+
):
455+
expression.meta["nonnull"] = True
456+
449457
return expression
450458

451459
def _annotate_unary(self, expression: E) -> E:
@@ -456,6 +464,9 @@ def _annotate_unary(self, expression: E) -> E:
456464
else:
457465
self._set_type(expression, expression.this.type)
458466

467+
if expression.this.meta.get("nonnull") is True:
468+
expression.meta["nonnull"] = True
469+
459470
return expression
460471

461472
def _annotate_literal(self, expression: exp.Literal) -> exp.Literal:
@@ -466,6 +477,8 @@ def _annotate_literal(self, expression: exp.Literal) -> exp.Literal:
466477
else:
467478
self._set_type(expression, exp.DataType.Type.DOUBLE)
468479

480+
expression.meta["nonnull"] = True
481+
469482
return expression
470483

471484
def _annotate_with_type(

sqlglot/optimizer/simplify.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,14 +395,15 @@ def remove_complements(expression, root=True):
395395
"""
396396
Removing complements.
397397
398-
A AND NOT A -> FALSE
399-
A OR NOT A -> TRUE
398+
A AND NOT A -> FALSE (only for non-NULL A)
399+
A OR NOT A -> TRUE (only for non-NULL A)
400400
"""
401401
if isinstance(expression, AND_OR) and (root or not expression.same_parent):
402402
ops = set(expression.flatten())
403403
for op in ops:
404404
if isinstance(op, exp.Not) and op.this in ops:
405-
return exp.false() if isinstance(expression, exp.And) else exp.true()
405+
if expression.meta.get("nonnull") is True:
406+
return exp.false() if isinstance(expression, exp.And) else exp.true()
406407

407408
return expression
408409

tests/fixtures/optimizer/simplify.sql

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ y OR y;
88
y;
99

1010
x AND NOT x;
11-
FALSE;
11+
NOT x AND x;
1212

1313
x OR NOT x;
14-
TRUE;
14+
NOT x OR x;
1515

1616
1 AND TRUE;
1717
TRUE;
@@ -299,7 +299,7 @@ A XOR D XOR B XOR E XOR F XOR G XOR C;
299299
A XOR B XOR C XOR D XOR E XOR F XOR G;
300300

301301
A AND NOT B AND C AND B;
302-
FALSE;
302+
A AND B AND C AND NOT B;
303303

304304
(a AND b AND c AND d) AND (d AND c AND b AND a);
305305
a AND b AND c AND d;
@@ -892,7 +892,7 @@ COALESCE(x, 1) = 1;
892892
x = 1 OR x IS NULL;
893893

894894
COALESCE(x, 1) IS NULL;
895-
FALSE;
895+
NOT x IS NULL AND x IS NULL;
896896

897897
COALESCE(ROW() OVER (), 1) = 1;
898898
ROW() OVER () = 1 OR ROW() OVER () IS NULL;
@@ -1344,3 +1344,30 @@ WITH t0 AS (SELECT 1 AS a, 'foo' AS p) SELECT NOT NOT CASE WHEN t0.a > 1 THEN t0
13441344
# dialect: sqlite
13451345
WITH t0 AS (SELECT 1 AS a, 'foo' AS p) SELECT NOT (NOT(CASE WHEN t0.a > 1 THEN t0.a ELSE t0.p END)) AS res FROM t0;
13461346
WITH t0 AS (SELECT 1 AS a, 'foo' AS p) SELECT NOT NOT CASE WHEN t0.a > 1 THEN t0.a ELSE t0.p END AS res FROM t0;
1347+
1348+
--------------------------------------
1349+
-- Simplify complements
1350+
--------------------------------------
1351+
TRUE OR NOT TRUE;
1352+
TRUE;
1353+
1354+
TRUE AND NOT TRUE;
1355+
FALSE;
1356+
1357+
'a' OR NOT 'a';
1358+
TRUE;
1359+
1360+
'a' AND NOT 'a';
1361+
FALSE;
1362+
1363+
100 OR NOT 100;
1364+
TRUE;
1365+
1366+
100 AND NOT 100;
1367+
FALSE;
1368+
1369+
NULL OR NOT NULL;
1370+
NULL;
1371+
1372+
NULL AND NOT NULL;
1373+
NULL;

tests/test_optimizer.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1699,3 +1699,58 @@ def test_annotate_object_construct(self):
16991699
self.assertEqual(
17001700
annotated.selects[0].type.sql("snowflake"), 'OBJECT("foo" VARCHAR, "a b" VARCHAR)'
17011701
)
1702+
1703+
def test_nonnull_annotation(self):
1704+
for literal_sql in ("1", "'foo'", "2.5"):
1705+
with self.subTest(f"Test NULL annotation for literal: {literal_sql}"):
1706+
sql = f"SELECT {literal_sql}"
1707+
query = parse_one(sql)
1708+
annotated = annotate_types(query)
1709+
assert annotated.selects[0].meta.get("nonnull") is True
1710+
1711+
schema = {"foo": {"id": "INT"}}
1712+
1713+
operand_pairs = (
1714+
("1", "1", True),
1715+
("foo.id", "foo.id", None),
1716+
("1", "foo.id", None),
1717+
("foo.id", "1", None),
1718+
)
1719+
1720+
for predicate in (">", "<", ">=", "<=", "=", "!=", "<>", "LIKE", "NOT LIKE"):
1721+
for operand1, operand2, nonnull in operand_pairs:
1722+
sql_predicate = f"{operand1} {predicate} {operand2}"
1723+
with self.subTest(f"Test NULL propagation for predicate: {predicate}"):
1724+
sql = f"SELECT {sql_predicate} FROM foo"
1725+
query = parse_one(sql)
1726+
annotated = annotate_types(query, schema=schema)
1727+
assert annotated.selects[0].meta.get("nonnull") is nonnull
1728+
1729+
for predicate in ("IS NULL", "IS NOT NULL"):
1730+
sql_predicate = f"foo.id {predicate}"
1731+
with self.subTest(f"Test NULL propagation for predicate: {predicate}"):
1732+
sql = f"SELECT {sql_predicate} FROM foo"
1733+
query = parse_one(sql)
1734+
annotated = annotate_types(query, schema=schema)
1735+
assert annotated.selects[0].meta.get("nonnull") is True
1736+
1737+
for connector in ("AND", "OR"):
1738+
for predicate in (">", "<", ">=", "<=", "=", "!=", "<>", "LIKE", "NOT LIKE"):
1739+
for operand1, operand2, nonnull in operand_pairs:
1740+
sql_predicate = f"({operand1} {predicate} {operand2})"
1741+
sql_connector = f"{sql_predicate} {connector} {sql_predicate}"
1742+
with self.subTest(
1743+
f"Test NULL propagation for connector: {connector} with predicates: {predicate}"
1744+
):
1745+
sql = f"SELECT {sql_connector} FROM foo"
1746+
query = parse_one(sql)
1747+
annotated = annotate_types(query, schema=schema)
1748+
assert annotated.selects[0].meta.get("nonnull") is nonnull
1749+
1750+
for unary in ("NOT", "-"):
1751+
for value, nonnull in (("1", True), ("foo.id", None)):
1752+
with self.subTest(f"Test NULL propagation for unary: {unary} with value: {value}"):
1753+
sql = f"SELECT {unary} {value} FROM foo"
1754+
query = parse_one(sql)
1755+
annotated = annotate_types(query, schema=schema)
1756+
assert annotated.selects[0].meta.get("nonnull") is nonnull

0 commit comments

Comments
 (0)