Repository: tobymao/sqlglot

Issue:
Optimizer Error - CAST(string column) + time interval
When using `CAST(col AS datetime/timestamp/date [at least this ones... there may be more]) + INTERVAL `

the optimizer gives some errors...  

**if cast to datetime (eg. duckdb)/timestamp (eg. postgres):**
```
print(
    optimize(
        sqlglot.parse_one("""
            select cast(Z as datetime) + INTERVAL '5' MONTH from x
        """),
        schema={"x": {"A": "INT",  "Z": "STRING"}}
    ).sql(pretty=True)
)
```
```
print(
    optimize(
        sqlglot.parse_one("""
            select cast(Z as timestamp) - INTERVAL '5' DAY from x
        """),
        schema={"x": {"A": "INT",  "Z": "STRING"}}
    ).sql(pretty=True)
)
```
```
  File "abc/python3.11/site-packages/sqlglot/optimizer/simplify.py", line 366, in _simplify_binary
    return date_literal(a + b)
                        ~~^~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'relativedelta'
```
**if cast to date:**
```
print(
    optimize(
        sqlglot.parse_one("""
            select cast(Z as date) + INTERVAL '5' DAY from x
        """),
        schema={"x": {"A": "INT",  "Z": "STRING"}}
    ).sql(pretty=True)
)
```
```
  File "abc/python3.11/site-packages/sqlglot/optimizer/simplify.py", line 428, in extract_date
    return datetime.date.fromisoformat(cast.name)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: Invalid isoformat string: 'z'
```



Discussion:
Thanks for the report, I'll take a look soon.

Resolving patch:
diff --git a/sqlglot/optimizer/simplify.py b/sqlglot/optimizer/simplify.py
--- a/sqlglot/optimizer/simplify.py
+++ b/sqlglot/optimizer/simplify.py
@@ -361,7 +361,7 @@ def _simplify_binary(expression, a, b):
             return boolean
     elif isinstance(a, exp.Cast) and isinstance(b, exp.Interval):
         a, b = extract_date(a), extract_interval(b)
-        if b:
+        if a and b:
             if isinstance(expression, exp.Add):
                 return date_literal(a + b)
             if isinstance(expression, exp.Sub):
@@ -369,7 +369,7 @@ def _simplify_binary(expression, a, b):
     elif isinstance(a, exp.Interval) and isinstance(b, exp.Cast):
         a, b = extract_interval(a), extract_date(b)
         # you cannot subtract a date from an interval
-        if a and isinstance(expression, exp.Add):
+        if a and b and isinstance(expression, exp.Add):
             return date_literal(a + b)
 
     return None
@@ -424,9 +424,15 @@ def eval_boolean(expression, a, b):
 
 
 def extract_date(cast):
-    if cast.args["to"].this == exp.DataType.Type.DATE:
-        return datetime.date.fromisoformat(cast.name)
-    return None
+    # The "fromisoformat" conversion could fail if the cast is used on an identifier,
+    # so in that case we can't extract the date.
+    try:
+        if cast.args["to"].this == exp.DataType.Type.DATE:
+            return datetime.date.fromisoformat(cast.name)
+        if cast.args["to"].this == exp.DataType.Type.DATETIME:
+            return datetime.datetime.fromisoformat(cast.name)
+    except ValueError:
+        return None
 
 
 def extract_interval(interval):
@@ -450,7 +456,8 @@ def extract_interval(interval):
 
 
 def date_literal(date):
-    return exp.Cast(this=exp.Literal.string(date), to=exp.DataType.build("DATE"))
+    expr_type = exp.DataType.build("DATETIME" if isinstance(date, datetime.datetime) else "DATE")
+    return exp.Cast(this=exp.Literal.string(date), to=expr_type)
 
 
 def boolean_literal(condition):


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
@@ -375,6 +375,18 @@ CAST('1998-12-01' AS DATE) - INTERVAL '90' foo;
 date '1998-12-01' + interval '90' foo;
 CAST('1998-12-01' AS DATE) + INTERVAL '90' foo;
 
+CAST(x AS DATE) + interval '1' week;
+CAST(x AS DATE) + INTERVAL '1' week;
+
+CAST('2008-11-11' AS DATETIME) + INTERVAL '5' MONTH;
+CAST('2009-04-11 00:00:00' AS DATETIME);
+
+datetime '1998-12-01' - interval '90' day;
+CAST('1998-09-02 00:00:00' AS DATETIME);
+
+CAST(x AS DATETIME) + interval '1' week;
+CAST(x AS DATETIME) + INTERVAL '1' week;
+
 --------------------------------------
 -- Comparisons
 --------------------------------------