Repository: ibis-project/ibis

Issue:
bug: Timestamp.time() errors on translation to SQL
### What happened?

It seems that getting a `TimeColumn` from a `TimestampColumn` fails on translation to SQL

Setup
```py
import ibis
import ibis.expr.datatypes as dt

t = ibis.table([("d", dt.timestamp)], "tbl")
expr = t[getattr(t.d.time(), "minute")()]
```

Problematic command:
```py
ibis.to_sql(expr)
```

### What version of ibis are you using?

4.1.0

### What backend(s) are you using, if any?

default

### Relevant log output

```sh
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/expr/sql.py", line 385, in to_sql
    compiled = backend.compile(expr)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/__init__.py", line 331, in compile
    return self.compiler.to_ast_ensure_limit(expr, limit, params=params).compile()
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py", line 40, in compile
    compiled_queries = [q.compile() for q in self.queries]
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/base.py", line 40, in <listcomp>
    compiled_queries = [q.compile() for q in self.queries]
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py", line 169, in compile
    frag = step(frag)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/query_builder.py", line 199, in _add_select
    arg = self._translate(op, named=True)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/query_builder.py", line 214, in _translate
    return translator.get_result()
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py", line 211, in get_result
    translated = self.translate(self.node)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py", line 243, in translate
    return formatter(self, op)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/alchemy/registry.py", line 489, in translator
    return sa.cast(sa.extract(fmt, t.translate(op.arg)), sa.SMALLINT)
  File "/opt/homebrew/lib/python3.10/site-packages/ibis/backends/base/sql/compiler/translator.py", line 245, in translate
    raise com.OperationNotDefinedError(f'No translation rule for {type(op)}')
ibis.common.exceptions.OperationNotDefinedError: No translation rule for <class 'ibis.expr.operations.temporal.Time'>
```
```


### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion:
Please tell me if I'm just holding it wrong, could very well be the case :)
@Tishj Thanks for the report!

The default backend used to compile an expression is DuckDB, and in this case it looks like the `Time` operation may not be implemented.

We'll look at getting this into 5.0.
@Tishj For your exact example here, you can just call `.minute()` directly on a `timestamp` column. There's no need to first extract the `time` component.

Resolving patch:
diff --git a/ibis/backends/base/sql/alchemy/registry.py b/ibis/backends/base/sql/alchemy/registry.py
--- a/ibis/backends/base/sql/alchemy/registry.py
+++ b/ibis/backends/base/sql/alchemy/registry.py
@@ -624,6 +624,7 @@ class array_filter(FunctionElement):
     ops.ExtractHour: _extract('hour'),
     ops.ExtractMinute: _extract('minute'),
     ops.ExtractSecond: _extract('second'),
+    ops.Time: fixed_arity(lambda arg: sa.cast(arg, sa.TIME), 1),
 }
 
 
diff --git a/ibis/backends/polars/compiler.py b/ibis/backends/polars/compiler.py
--- a/ibis/backends/polars/compiler.py
+++ b/ibis/backends/polars/compiler.py
@@ -995,3 +995,11 @@ def execute_e(op, **kwargs):
 @translate.register(ops.Pi)
 def execute_pi(op, **kwargs):
     return pl.lit(np.pi)
+
+
+@translate.register(ops.Time)
+def execute_time(op, **kwargs):
+    arg = translate(op.arg, **kwargs)
+    if op.arg.output_dtype.is_timestamp():
+        return arg.dt.truncate("1us").cast(pl.Time)
+    return arg


Test patch:
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
@@ -8,7 +8,7 @@
 import pandas as pd
 import pandas.testing as tm
 import pytest
-import sqlalchemy.exc
+import sqlalchemy as sa
 from pytest import param
 
 import ibis
@@ -938,6 +938,55 @@ def test_time_literal(con, backend):
         assert con.execute(expr.typeof()) == TIME_BACKEND_TYPES[backend_name]
 
 
+@pytest.mark.notyet(
+    ["clickhouse", "impala", "pyspark"],
+    raises=com.OperationNotDefinedError,
+    reason="backend doesn't have a time datatype",
+)
+@pytest.mark.notyet(
+    ["druid"],
+    raises=sa.exc.CompileError,
+    reason="druid sqlalchemy dialect fails to compile datetime types",
+)
+@pytest.mark.broken(
+    ["sqlite"], raises=AssertionError, reason="SQLite returns Timedelta from execution"
+)
+@pytest.mark.notimpl(
+    ["dask", "datafusion", "pandas"], raises=com.OperationNotDefinedError
+)
+@pytest.mark.parametrize(
+    "microsecond",
+    [
+        0,
+        param(
+            561021,
+            marks=[
+                pytest.mark.notimpl(
+                    ["mssql", "mysql"],
+                    raises=AssertionError,
+                    reason="doesn't have enough precision to capture microseconds",
+                ),
+                pytest.mark.notyet(
+                    ["trino"],
+                    raises=AssertionError,
+                    reason="has enough precision, but sqlalchemy dialect drops them",
+                ),
+            ],
+        ),
+    ],
+    ids=["second", "subsecond"],
+)
+def test_extract_time_from_timestamp(con, microsecond):
+    raw_ts = datetime.datetime(2023, 1, 7, 13, 20, 5, microsecond)
+    ts = ibis.timestamp(raw_ts)
+    expr = ts.time()
+
+    result = con.execute(expr)
+    expected = raw_ts.time()
+
+    assert result == expected
+
+
 INTERVAL_BACKEND_TYPES = {
     'bigquery': "INTERVAL",
     'clickhouse': 'IntervalSecond',
@@ -952,7 +1001,7 @@ def test_time_literal(con, backend):
     ['snowflake'],
     '(snowflake.connector.errors.ProgrammingError) 001007 (22023): SQL compilation error:'
     "invalid type [CAST(INTERVAL_LITERAL('second', '1') AS VARIANT)] for parameter 'TO_VARIANT'",
-    raises=sqlalchemy.exc.ProgrammingError,
+    raises=sa.exc.ProgrammingError,
 )
 @pytest.mark.broken(
     ['impala'],