Repository: ibis-project/ibis

Issue:
feat(impala): support dates
### What happened?

Unable to cast `timestamp` columns to `date` directly using the Impala backend. Here's a reproducible example:

```python

import ibis
ibis.options.interactive = True

df = ibis.memtable({
    'timestamp': ['2023-12-21 09:30:00']
})

to_date = lambda d: ibis.to_sql(df.timestamp.cast('date'), dialect=d)

# DuckDB 
to_date('duckdb')
# SELECT
#   CAST(t0.timestamp AS DATE) AS "Cast(timestamp, date)"
# FROM _ibis_pandas_memtable_y574b7whvjffrllsztk4jevkiu AS t0

# MySQL
to_date('mysql')
# SELECT
#   CAST(t0.timestamp AS DATE) AS `Cast(timestamp, date)`
# FROM (
#   SELECT
#     '2023-12-21 09:30:00' AS timestamp
# ) AS t0

# Impala
to_date('impala')
# KeyError: 'date'
# UnsupportedBackendType: date
#
# But both CAST(timestamp_column AS DATE) and TO_DATE(timestamp_column) is possible in Impala.
```

### What version of ibis are you using?

`8.0.0.dev321` on Windows 10

### What backend(s) are you using, if any?

Impala

### Relevant log output

```sh
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File .venv\lib\site-packages\ibis\backends\base\sql\registry\helpers.py:78, in type_to_sql_string(tval)
     77 try:
---> 78     return sql_type_names[name]
     79 except KeyError:

KeyError: 'date'

During handling of the above exception, another exception occurred:

UnsupportedBackendType                    Traceback (most recent call last)
Cell In[12], line 1
----> 1 to_date('impala')

Cell In[9], line 1
----> 1 to_date = lambda d: ibis.to_sql(df.timestamp.cast('date'), dialect=d)

File .venv\lib\site-packages\ibis\expr\sql.py:399, in to_sql(expr, dialect, **kwargs)
    396     else:
    397         read = write = getattr(backend, "_sqlglot_dialect", dialect)
--> 399 sql = backend._to_sql(expr, **kwargs)
    400 (pretty,) = sg.transpile(sql, read=read, write=write, pretty=True)
    401 return SQLString(pretty)

File .venv\lib\site-packages\ibis\backends\base\sql\__init__.py:392, in BaseSQLBackend._to_sql(self, expr, **kwargs)
    391 def _to_sql(self, expr: ir.Expr, **kwargs) -> str:
--> 392     return str(self.compile(expr, **kwargs))

File .venv\lib\site-packages\ibis\backends\base\sql\__init__.py:389, in BaseSQLBackend.compile(self, expr, limit, params, timecontext)
    368 """Compile an Ibis expression.
    369 
    370 Parameters
   (...)
    386     backend.
    387 """
    388 self._define_udf_translation_rules(expr)
--> 389 return self.compiler.to_ast_ensure_limit(expr, limit, params=params).compile()

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\base.py:37, in QueryAST.compile(self)
     35 def compile(self):
     36     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 37     compiled_queries = [q.compile() for q in self.queries]
     38     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     39     return self.context.collapse(
     40         list(
     41             chain(
   (...)
     46         )
     47     )

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\base.py:37, in <listcomp>(.0)
     35 def compile(self):
     36     compiled_setup_queries = [q.compile() for q in self.setup_queries]
---> 37     compiled_queries = [q.compile() for q in self.queries]
     38     compiled_teardown_queries = [q.compile() for q in self.teardown_queries]
     39     return self.context.collapse(
     40         list(
     41             chain(
   (...)
     46         )
     47     )

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\query_builder.py:266, in Select.compile(self)
    263 with_frag = self.format_subqueries()
    265 # SELECT
--> 266 select_frag = self.format_select_set()
    268 # FROM, JOIN, UNION
    269 from_frag = self.format_table_set()

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\query_builder.py:321, in Select.format_select_set(self)
    319 for node in self.select_set:
    320     if isinstance(node, ops.Value):
--> 321         expr_str = self._translate(node, named=True, permit_subquery=True)
    322     elif isinstance(node, ops.TableNode):
    323         alias = context.get_ref(node)

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\query_builder.py:236, in Select._translate(self, expr, named, permit_subquery, within_where)
    228 def _translate(self, expr, named=False, permit_subquery=False, within_where=False):
    229     translator = self.translator_class(
    230         expr,
    231         context=self.context,
   (...)
    234         within_where=within_where,
    235     )
--> 236     return translator.get_result()

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\translator.py:224, in ExprTranslator.get_result(self)
    222 def get_result(self):
    223     """Compile SQL expression into a string."""
--> 224     translated = self.translate(self.node)
    225     if self._needs_name(self.node):
    226         # TODO: this could fail in various ways
    227         translated = self.name(translated, self.node.name)

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\translator.py:256, in ExprTranslator.translate(self, op)
    254 elif type(op) in self._registry:
    255     formatter = self._registry[type(op)]
--> 256     return formatter(self, op)
    257 else:
    258     raise com.OperationNotDefinedError(f"No translation rule for {type(op)}")

File .venv\lib\site-packages\ibis\backends\base\sql\registry\main.py:23, in alias(translator, op)
     20 def alias(translator, op):
     21     # just compile the underlying argument because the naming is handled
     22     # by the translator for the top level expression
---> 23     return translator.translate(op.arg)

File .venv\lib\site-packages\ibis\backends\base\sql\compiler\translator.py:256, in ExprTranslator.translate(self, op)
    254 elif type(op) in self._registry:
    255     formatter = self._registry[type(op)]
--> 256     return formatter(self, op)
    257 else:
    258     raise com.OperationNotDefinedError(f"No translation rule for {type(op)}")

File .venv\lib\site-packages\ibis\backends\base\sql\registry\main.py:109, in cast(translator, op)
    107     return f"1000000 * unix_timestamp({arg_formatted})"
    108 else:
--> 109     sql_type = helpers.type_to_sql_string(op.to)
    110     return f"CAST({arg_formatted} AS {sql_type})"

File .venv\lib\site-packages\ibis\backends\base\sql\registry\helpers.py:80, in type_to_sql_string(tval)
     78     return sql_type_names[name]
     79 except KeyError:
---> 80     raise com.UnsupportedBackendType(name)

UnsupportedBackendType: date
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
@chris-park Thanks for the issue!

It wasn't until recently that we tested against a version of Impala that supported dates. I expect that it will take a bit of effort to flesh out date support here.

Can you talk a bit about how you're using Impala?

We'd generally like to avoid adding new features to Impala because its upstream development status is hard to understand.

If you're using this in a production setting we'd like to know and if you're exploring Ibis then we'd also like to know that as well!

Resolving patch:
diff --git a/ibis/backends/base/sql/registry/helpers.py b/ibis/backends/base/sql/registry/helpers.py
--- a/ibis/backends/base/sql/registry/helpers.py
+++ b/ibis/backends/base/sql/registry/helpers.py
@@ -67,6 +67,7 @@ def needs_parens(op: ops.Node):
     "boolean": "boolean",
     "timestamp": "timestamp",
     "decimal": "decimal",
+    "date": "date",
 }
 
 


Test patch:
diff --git a/ibis/backends/tests/test_param.py b/ibis/backends/tests/test_param.py
--- a/ibis/backends/tests/test_param.py
+++ b/ibis/backends/tests/test_param.py
@@ -204,7 +204,6 @@ def test_scalar_param(backend, alltypes, df, value, dtype, col):
     ids=["string", "date", "datetime"],
 )
 @pytest.mark.notimpl(["druid", "oracle"])
-@pytest.mark.notyet(["impala"], reason="impala doesn't support dates")
 def test_scalar_param_date(backend, alltypes, value):
     param = ibis.param("date")
     ds_col = alltypes.date_string_col
diff --git a/ibis/backends/tests/test_temporal.py b/ibis/backends/tests/test_temporal.py
--- a/ibis/backends/tests/test_temporal.py
+++ b/ibis/backends/tests/test_temporal.py
@@ -81,14 +81,7 @@
 @pytest.mark.parametrize("attr", ["year", "month", "day"])
 @pytest.mark.parametrize(
     "expr_fn",
-    [
-        param(lambda c: c.date(), id="date"),
-        param(
-            lambda c: c.cast("date"),
-            id="cast",
-            marks=pytest.mark.notimpl(["impala"], raises=com.UnsupportedBackendType),
-        ),
-    ],
+    [param(lambda c: c.date(), id="date"), param(lambda c: c.cast("date"), id="cast")],
 )
 @pytest.mark.notimpl(
     ["druid"],
@@ -1957,7 +1950,6 @@ def test_string_to_timestamp(alltypes, fmt):
     ],
 )
 @pytest.mark.notimpl(["mssql", "druid", "oracle"], raises=com.OperationNotDefinedError)
-@pytest.mark.notimpl(["impala"], raises=com.UnsupportedBackendType)
 @pytest.mark.notimpl(
     ["flink"],
     raises=Py4JJavaError,
@@ -2494,7 +2486,6 @@ def test_timestamp_column_from_ymdhms(backend, con, alltypes, df):
         "java.lang.UnsupportedOperationException: class org.apache.calcite.sql.SqlIdentifier: LONG"
     ),
 )
-@pytest.mark.notimpl(["impala"], raises=com.UnsupportedBackendType)
 @pytest.mark.notimpl(
     ["oracle"], raises=sa.exc.DatabaseError, reason="ORA-01861 literal does not match"
 )
@@ -2512,7 +2503,6 @@ def test_date_scalar_from_iso(con):
     raises=sa.exc.ProgrammingError,
     reason="java.lang.UnsupportedOperationException: class org.apache.calcite.sql.SqlIdentifier: STRING",
 )
-@pytest.mark.notimpl(["impala"], raises=com.UnsupportedBackendType)
 @pytest.mark.notyet(
     ["oracle"],
     raises=sa.exc.DatabaseError,
@@ -2648,11 +2638,6 @@ def build_date_col(t):
     raises=sa.exc.CompileError,
     reason='No literal value renderer is available for literal value "datetime.date(2010, 11, 1)" with datatype DATE',
 )
-@pytest.mark.notyet(
-    ["impala"],
-    reason="impala doesn't support dates",
-    raises=com.UnsupportedBackendType,
-)
 @pytest.mark.notimpl(["oracle"], raises=sa.exc.DatabaseError)
 @pytest.mark.parametrize(
     ("left_fn", "right_fn"),