Repository: tobymao/sqlglot

Issue:
feature: optimizer: statically analyze conditions to check for literals 
**Is your feature request related to a problem? Please describe.**
[Ibis sometimes generates fairly unoptimized SQL because it doesn't distinguish well between literals known at compile time, and values that could only be known at run time](https://github.com/ibis-project/ibis/pull/8786). Many times this could be avoided though. Note in that issue the `... CASE WHEN CAST(1 AS TINYINT) >= 0 ...`. This is trivial to see that this is always true. If we could statically analyze the expression and determine this is always true or always false, then Ibis could avoid a lot of the branching.

**Describe the solution you'd like**
I think plugging this into the optimizer system.

**Describe alternatives you've considered**
In ibis we could keep track of if an expression is a literal ourselves, but then this would require use to carry around a bit of extra metadata around during the compilation process. This

**Additional context**
The simplest case would be be to optimize `CAST(1 AS TINYINT) >= 0` to `TRUE`. An extension could be to actually optimize away parent `CASE` conditions etc that use the condition. But that could be a followup, this first step would be adequate to solve our needs in ibis.

If you think this is a good feature, I would be happy to help with it, but with the caveat that I am unfamiliar with this codebase so it would be difficult for me. If you wanted to be a hero, it would be much appreciated :)



Discussion:
This is an interesting one. Perhaps we could augment the `simplify` [rule](https://github.com/tobymao/sqlglot/blob/main/sqlglot/optimizer/simplify.py) to handle this as well. It already simplifies `1 >= 0`.

I wonder if there's any weird edge case where ignoring the `CAST` and simply comparing the value directly is unsafe.
> An extension could be to actually optimize away parent CASE conditions etc that use the condition. But that could be a followup, this first step would be adequate to solve our needs in ibis.

We also have `CASE` simplification, and since these rules are applied until we reach a fix point, implementing the transformation you suggested here would suffice.

```python
>>> from sqlglot import parse_one
>>> from sqlglot.optimizer.simplify import simplify
>>> simplify(parse_one("case when true then 1 else 2 end")).sql()
'1'
```
i think we do something similar with cast as date

if there’s a cast against a predicate/condition, it’s probably safe to remove if there’s no overflow 
@VaggelisD wanna take a stab at this? We could chat about it tomorrow.
Most of the situations that I'm trying to optimize in ibis are with "small" numbers eg 0, 1, 2, -1. All of these should fit into the smallest precision integer. If you just solved these few simple cases I bet it would fix 95% of the cases I'm running into in Ibis, IDK if you want to add in those edge cases, or if you would only want to keep your code clean and solve it in the general case.

Also, I could take a look more at removing these CASTs on the ibis side. I didn't realize `1 >= 0` was already implemented here. So if I remove the CAST on the ibis side, I should pickup the optimization with no changes needed here on the sqlglot side.
cc @cpcloud 
that’s right look at simplify. 
Hi everyone,

This is an interesting problem. I was considering an initial prototype with reduced scope for now e.g. simplifying `CAST(<integer literal> AS <integer type>)` when present in binary expressions, which would also enable Ibis to statically analyze these conditions. The implementation for just that is straightforward, but I can't help but think that we're opening a channel of potential side-effects if we're not careful. In general, I think even for this simple case we'd have to pick from 3 possible paths:

1. Allow any integer literal and reason whether the cast to `<integer type>` would overflow. This inference can be complex as dialects could have differences in the integer types (byte size, signedness etc), so making a global choice without these specifics will not be feasible. However, even if all major dialects have equivalent types, this would bring additional complexity to this optimization as it'd require to create and maintain "metadata" for each `DataType`.

2. Treat it as a "small integer" optimization for now and remove the cast only if the `<integer literal>` value can fit in byte. This still isn't entirely safe e.g. `CAST(-1 AS UNSIGNED) != -1`, but at the very least we're now constrained to byte range and we'd only have to reason whether `<integer type>` is signed or not, which is a simpler case of (1)

3. Ignore the overflow case and always remove the casts; While sqlglot treats malformed input as UB in transpilation/parsing, the same can't be said for the optimizer which must preserve correctness at all times in my opinion

Personally, I'm a fan of (2) as it's the middle ground and could be prototyped by the end of the day; In a way, it's also a simplified version of (1) which could be implemented in the future to extend the range of values for this optimization. Any opinions?
the optimizer has access to the dialect. i'm not exactly sure what you mean by fit into a byte, is that 255? 

whatever the solution is, it needs to be 100% safe, so if you're not going to handle unsigned, don't optimize it.
Even if we handled ints [0, 127] that would help the ibis case. All those numbers can be represented safely as int8s and uint8s, so I think we wouldn't have to consider the precision of the data type there (who knows, someone could implement some heinous SQL engine that can use 4 bit ints and then this would break them, but I say if ore that possibility).

Also could handle "CAST int AS bool" case safely regardless of the dialect?

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -3921,15 +3921,17 @@ class Type(AutoName):
         Type.NAME,
     }
 
-    INTEGER_TYPES = {
+    SIGNED_INTEGER_TYPES = {
         Type.BIGINT,
-        Type.BIT,
         Type.INT,
         Type.INT128,
         Type.INT256,
         Type.MEDIUMINT,
         Type.SMALLINT,
         Type.TINYINT,
+    }
+
+    UNSIGNED_INTEGER_TYPES = {
         Type.UBIGINT,
         Type.UINT,
         Type.UINT128,
@@ -3939,6 +3941,12 @@ class Type(AutoName):
         Type.UTINYINT,
     }
 
+    INTEGER_TYPES = {
+        *SIGNED_INTEGER_TYPES,
+        *UNSIGNED_INTEGER_TYPES,
+        Type.BIT,
+    }
+
     FLOAT_TYPES = {
         Type.DOUBLE,
         Type.FLOAT,
diff --git a/sqlglot/optimizer/simplify.py b/sqlglot/optimizer/simplify.py
--- a/sqlglot/optimizer/simplify.py
+++ b/sqlglot/optimizer/simplify.py
@@ -22,6 +22,12 @@
 # Final means that an expression should not be simplified
 FINAL = "final"
 
+# Value ranges for byte-sized signed/unsigned integers
+TINYINT_MIN = -128
+TINYINT_MAX = 127
+UTINYINT_MIN = 0
+UTINYINT_MAX = 255
+
 
 class UnsupportedUnit(Exception):
     pass
@@ -547,7 +553,34 @@ def simplify_literals(expression, root=True):
 NULL_OK = (exp.NullSafeEQ, exp.NullSafeNEQ, exp.PropertyEQ)
 
 
+def _simplify_integer_cast(expr: exp.Expression) -> exp.Expression:
+    if isinstance(expr, exp.Cast) and isinstance(expr.this, exp.Cast):
+        this = _simplify_integer_cast(expr.this)
+    else:
+        this = expr.this
+
+    if isinstance(expr, exp.Cast) and this.is_int:
+        num = int(this.name)
+
+        # Remove the (up)cast from small (byte-sized) integers in predicates which is side-effect free. Downcasts on any
+        # integer type might cause overflow, thus the cast cannot be eliminated and the behavior is
+        # engine-dependent
+        if (
+            TINYINT_MIN <= num <= TINYINT_MAX and expr.to.this in exp.DataType.SIGNED_INTEGER_TYPES
+        ) or (
+            UTINYINT_MIN <= num <= UTINYINT_MAX
+            and expr.to.this in exp.DataType.UNSIGNED_INTEGER_TYPES
+        ):
+            return this
+
+    return expr
+
+
 def _simplify_binary(expression, a, b):
+    if isinstance(expression, COMPARISONS):
+        a = _simplify_integer_cast(a)
+        b = _simplify_integer_cast(b)
+
     if isinstance(expression, exp.Is):
         if isinstance(b, exp.Not):
             c = b.this
diff --git a/sqlglot/tokens.py b/sqlglot/tokens.py
--- a/sqlglot/tokens.py
+++ b/sqlglot/tokens.py
@@ -798,6 +798,7 @@ class Tokenizer(metaclass=_Tokenizer):
         "LONG": TokenType.BIGINT,
         "BIGINT": TokenType.BIGINT,
         "INT8": TokenType.TINYINT,
+        "UINT": TokenType.UINT,
         "DEC": TokenType.DECIMAL,
         "DECIMAL": TokenType.DECIMAL,
         "BIGDECIMAL": TokenType.BIGDECIMAL,


Test patch:
diff --git a/tests/fixtures/optimizer/simplify.sql b/tests/fixtures/optimizer/simplify.sql
--- a/tests/fixtures/optimizer/simplify.sql
+++ b/tests/fixtures/optimizer/simplify.sql
@@ -715,6 +715,45 @@ FUN() > 0;
 RAND() > 0 OR RAND() > 1;
 RAND() > 0 OR RAND() > 1;
 
+CAST(1 AS UINT) >= 0;
+TRUE;
+
+CAST(-1 AS TINYINT) <= 0;
+TRUE;
+
+CAST(1 AS INT) = CAST(1 AS UINT);
+TRUE;
+
+CASE WHEN CAST(1 AS TINYINT) = 1 THEN FALSE ELSE TRUE END;
+FALSE;
+
+CAST(1 AS INT) + 1;
+CAST(1 AS INT) + 1;
+
+CAST(CAST(CAST(-1 AS INT) AS INT) AS INT) = -1;
+TRUE;
+
+CAST(-1 AS UINT) <= 0;
+CAST(-1 AS UINT) <= 0;
+
+CAST(-129 AS TINYINT) <= 0;
+CAST(-129 AS TINYINT) <= 0;
+
+CAST(256 AS UINT) >= 0;
+CAST(256 AS UINT) >= 0;
+
+CAST(CAST(CAST(-1 AS INT) AS UINT) AS INT) = 1;
+CAST(CAST(CAST(-1 AS INT) AS UINT) AS INT) = 1;
+
+CAST(x AS TINYINT) = 1;
+CAST(x AS TINYINT) = 1;
+
+CAST(CAST(1 AS INT) AS BOOLEAN) = 1;
+CAST(CAST(1 AS INT) AS BOOLEAN) = 1;
+
+CAST(CAST(CAST(1 AS INT) AS BOOLEAN) AS INT) = 1;
+CAST(CAST(CAST(1 AS INT) AS BOOLEAN) AS INT) = 1;
+
 --------------------------------------
 -- COALESCE
 --------------------------------------