Repository: modin-project/modin

Issue:
BUG: HDK: Unsupported dtype: datetime64[ns]
### Modin version checks

- [X] I have checked that this issue has not already been reported.

- [X] I have confirmed this bug exists on the latest released version of Modin.

- [X] I have confirmed this bug exists on the main branch of Modin. (In order to do this you can follow [this guide](https://modin.readthedocs.io/en/stable/getting_started/installation.html#installing-from-the-github-master-branch).)


### Reproducible Example

```python
import modin.pandas as pd

dates = [
    np.datetime64(1, "ns"),
    np.datetime64(2, "ns"),
]
df = pd.DataFrame({'date': dates})
print(df["date"].max())
```


### Issue Description

The code above fails with error:
```python-traceback
modin/experimental/core/execution/native/implementations/hdk_on_native/calcite_serializer.py", line 367, in serialize_dtype
    raise TypeError(f"Unsupported dtype: {dtype}")
TypeError: Unsupported dtype: datetime64[ns]
```

### Expected Behavior

1970-01-01 00:00:00.000000002

### Error Logs

<details>

```python-traceback

Replace this line with the error backtrace (if applicable).

```

</details>


### Installed Versions

<details>

Replace this line with the output of pd.show_versions()

</details>



Discussion:

Resolving patch:
diff --git a/modin/experimental/core/execution/native/implementations/hdk_on_native/calcite_serializer.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/calcite_serializer.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/calcite_serializer.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/calcite_serializer.py
@@ -13,6 +13,8 @@
 
 """Module provides ``CalciteSerializer`` class."""
 
+from pandas.core.dtypes.common import is_datetime64_dtype
+
 from .expr import (
     BaseExpr,
     LiteralExpr,
@@ -65,6 +67,7 @@ class CalciteSerializer:
         "bool": "BOOLEAN",
         "float32": "FLOAT",
         "float64": "DOUBLE",
+        "datetime64": "TIMESTAMP",
     }
 
     _INT_OPTS = {
@@ -79,6 +82,16 @@ class CalciteSerializer:
         int: ("BIGINT", 19),
     }
 
+    _TIMESTAMP_PRECISION = {
+        "s": 0,
+        "ms": 3,
+        "us": 6,
+        "ns": 9,
+    }
+    _DTYPE_STRINGS.update(
+        {f"datetime64[{u}]": "TIMESTAMP" for u in _TIMESTAMP_PRECISION}
+    )
+
     def serialize(self, plan):
         """
         Serialize a sequence of Calcite nodes into JSON format.
@@ -327,6 +340,20 @@ def serialize_literal(self, literal):
                 "type_scale": -2147483648,
                 "type_precision": 1,
             }
+        if isinstance(val, np.datetime64):
+            unit = np.datetime_data(val)[0]
+            precision = self._TIMESTAMP_PRECISION.get(unit, None)
+            if precision is not None:
+                return {
+                    "literal": int(val.astype(np.int64)),
+                    "type": "TIMESTAMP",
+                    "target_type": "TIMESTAMP",
+                    "scale": -2147483648,
+                    "precision": precision,
+                    "type_scale": -2147483648,
+                    "type_precision": precision,
+                }
+
         raise NotImplementedError(f"Can not serialize {type(val).__name__}")
 
     def opts_for_int_type(self, int_type):
@@ -367,7 +394,11 @@ def serialize_dtype(self, dtype):
         """
         _warn_if_unsigned(dtype)
         try:
-            return {"type": self._DTYPE_STRINGS[dtype.name], "nullable": True}
+            type_info = {"type": self._DTYPE_STRINGS[dtype.name], "nullable": True}
+            if is_datetime64_dtype(dtype):
+                unit = np.datetime_data(dtype)[0]
+                type_info["precision"] = self._TIMESTAMP_PRECISION[unit]
+            return type_info
         except KeyError:
             raise TypeError(f"Unsupported dtype: {dtype}")
 
diff --git a/modin/experimental/core/execution/native/implementations/hdk_on_native/expr.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/expr.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/expr.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/expr.py
@@ -31,6 +31,7 @@
     is_categorical_dtype,
     is_datetime64_any_dtype,
     is_bool_dtype,
+    is_datetime64_dtype,
 )
 
 from modin.utils import _inherit_docstrings
@@ -65,6 +66,8 @@ def _get_common_dtype(lhs_dtype, rhs_dtype):
         return get_dtype(float)
     if is_integer_dtype(lhs_dtype) and is_integer_dtype(rhs_dtype):
         return get_dtype(int)
+    if is_datetime64_dtype(lhs_dtype) and is_datetime64_dtype(rhs_dtype):
+        return np.promote_types(lhs_dtype, rhs_dtype)
     raise NotImplementedError(
         f"Cannot perform operation on types: {lhs_dtype}, {rhs_dtype}"
     )
@@ -801,45 +804,48 @@ class LiteralExpr(BaseExpr):
 
     Parameters
     ----------
-    val : int, np.int, float, bool, str or None
+    val : int, np.int, float, bool, str, np.datetime64 or None
         Literal value.
     dtype : None or dtype, default: None
         Value dtype.
 
     Attributes
     ----------
-    val : int, np.int, float, bool, str or None
+    val : int, np.int, float, bool, str, np.datetime64 or None
         Literal value.
     _dtype : dtype
         Literal data type.
     """
 
     def __init__(self, val, dtype=None):
-        if dtype is None:
-            if val is not None and not isinstance(
-                val,
-                (
-                    int,
-                    float,
-                    bool,
-                    str,
-                    np.int8,
-                    np.int16,
-                    np.int32,
-                    np.int64,
-                    np.uint8,
-                    np.uint16,
-                    np.uint32,
-                    np.uint64,
-                ),
-            ):
-                raise NotImplementedError(f"Literal value {val} of type {type(val)}")
-            if val is None:
-                dtype = get_dtype(float)
-            else:
-                dtype = get_dtype(type(val))
+        if val is not None and not isinstance(
+            val,
+            (
+                int,
+                float,
+                bool,
+                str,
+                np.int8,
+                np.int16,
+                np.int32,
+                np.int64,
+                np.uint8,
+                np.uint16,
+                np.uint32,
+                np.uint64,
+                np.datetime64,
+            ),
+        ):
+            raise NotImplementedError(f"Literal value {val} of type {type(val)}")
         self.val = val
-        self._dtype = dtype
+        if dtype is not None:
+            self._dtype = dtype
+        elif val is None:
+            self._dtype = get_dtype(float)
+        else:
+            self._dtype = (
+                val.dtype if isinstance(val, np.generic) else get_dtype(type(val))
+            )
 
     def copy(self):
         """
@@ -857,8 +863,21 @@ def fold(self):
 
     @_inherit_docstrings(BaseExpr.cast)
     def cast(self, res_type):
-        dtype = np.dtype(res_type)
-        return LiteralExpr(dtype.type(self.val), dtype)
+        val = self.val
+        if val is not None:
+            if isinstance(val, np.generic):
+                val = val.astype(res_type)
+            elif is_integer_dtype(res_type):
+                val = int(val)
+            elif is_float_dtype(res_type):
+                val = float(val)
+            elif is_bool_dtype(res_type):
+                val = bool(val)
+            elif is_string_dtype(res_type):
+                val = str(val)
+            else:
+                raise TypeError(f"Cannot cast '{val}' to '{res_type}'")
+        return LiteralExpr(val, res_type)
 
     @_inherit_docstrings(BaseExpr.is_null)
     def is_null(self):
@@ -1336,6 +1355,11 @@ def build_if_then_else(cond, then_val, else_val, res_type):
     BaseExpr
         The conditional operator expression.
     """
+    if is_datetime64_dtype(res_type):
+        if then_val._dtype != res_type:
+            then_val = then_val.cast(res_type)
+        if else_val._dtype != res_type:
+            else_val = else_val.cast(res_type)
     return OpExpr("CASE", [cond, then_val, else_val], res_type)
 
 


Test patch:
diff --git a/modin/experimental/core/execution/native/implementations/hdk_on_native/test/test_dataframe.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/test/test_dataframe.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/test/test_dataframe.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/test/test_dataframe.py
@@ -54,6 +54,9 @@
 from modin.experimental.core.execution.native.implementations.hdk_on_native.df_algebra import (
     FrameNode,
 )
+from modin.experimental.core.execution.native.implementations.hdk_on_native.calcite_serializer import (
+    CalciteSerializer,
+)
 
 
 # Our configuration in pytest.ini requires that we explicitly catch all
@@ -2020,6 +2023,33 @@ def dt_hour(df, **kwargs):
 
         run_and_compare(dt_hour, data=self.datetime_data)
 
+    @pytest.mark.parametrize("cast", [True, False])
+    @pytest.mark.parametrize("unit", CalciteSerializer._TIMESTAMP_PRECISION.keys())
+    def test_dt_serialization(self, cast, unit):
+        fill_value = np.datetime64(3, unit)
+
+        def serialize(df, **kwargs):
+            if cast:
+                df = df.astype(f"datetime64[{unit}]")
+            return df.fillna(fill_value)
+
+        def cmp(df1, df2):
+            assert df1["date"].max().asm8 == fill_value
+            assert df2["date"].max().asm8 == fill_value
+            df_equals(df1, df2)
+
+        run_and_compare(
+            serialize,
+            data={
+                "date": [
+                    np.datetime64(1, unit),
+                    np.datetime64(2, unit),
+                    None,
+                ]
+            },
+            comparator=cmp,
+        )
+
 
 class TestCategory:
     data = {