Repository: modin-project/modin

Issue:
BUG: HDK: read_csv() is unable to parse dates with nanosecond precision
### 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

path = "tmp.csv"
with open(path, "w") as file:
    file.write("id,date\n1,2023-01-01 00:00:01.000000000")
df = pd.read_csv(path, header=0, dtype={"id": "int64"}, parse_dates=["date"])
print(df)
print(df.dtypes["date"])
```


### Issue Description

The above example defaults to pandas, when both `dtype` and `parse_dates` argument are specified, because it's unable to parse date with nanosecond precision. If the dtype argument is not specified, the parse_dates argument is simply ignored.

### Expected Behavior

```
   id                date
0   1 2023-01-01 00:00:01
datetime64[ns]
```

### 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/io/io.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/io/io.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/io/io.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/io/io.py
@@ -19,6 +19,7 @@
 
 from csv import Dialect
 from typing import Union, Sequence, Callable, Dict, Tuple
+import functools
 import inspect
 import os
 
@@ -116,7 +117,8 @@ def read_csv(cls, **kwargs):  # noqa: PR01
         -----
         Reading performed by using of `pyarrow.read_csv` function.
         """
-        eng = str(kwargs["engine"]).lower().strip()
+        if eng := kwargs["engine"]:
+            eng = eng.lower().strip()
         try:
             if eng in ("pandas", "c"):
                 return super().read_csv(**kwargs)
@@ -128,22 +130,54 @@ def read_csv(cls, **kwargs):  # noqa: PR01
             if not use_modin_impl:
                 raise ArrowEngineException(error_message)
 
-            dtype = kwargs["dtype"]
-            if isinstance(dtype, dict):
-                column_types = {c: cls._dtype_to_arrow(t) for c, t in dtype.items()}
-            else:
-                column_types = cls._dtype_to_arrow(dtype)
-
-            parse_dates = kwargs["parse_dates"]
-            if (type(parse_dates) is list) and type(column_types) is dict:
-                for c in parse_dates:
-                    column_types[c] = pa.timestamp("s")
-
-            names = kwargs["names"]
+            if (names := kwargs["names"]) is lib.no_default:
+                names = None
             skiprows = kwargs["skiprows"]
-            if names not in (lib.no_default, None) and kwargs["header"] == 0:
+            if names and kwargs["header"] == 0:
                 skiprows = skiprows + 1 if skiprows is not None else 1
 
+            @functools.lru_cache(maxsize=None)
+            def get_col_names():
+                # Using pandas to read the column names
+                return pandas.read_csv(
+                    kwargs["filepath_or_buffer"], nrows=0, engine="c"
+                ).columns.tolist()
+
+            if dtype := kwargs["dtype"]:
+                if isinstance(dtype, dict):
+                    column_types = {c: cls._dtype_to_arrow(t) for c, t in dtype.items()}
+                else:
+                    dtype = cls._dtype_to_arrow(dtype)
+                    column_types = {name: dtype for name in get_col_names()}
+            else:
+                column_types = {}
+
+            if parse_dates := kwargs["parse_dates"]:
+                # Either list of column names or list of column indices is supported.
+                if isinstance(parse_dates, list) and (
+                    all(isinstance(col, str) for col in parse_dates)
+                    or all(isinstance(col, int) for col in parse_dates)
+                ):
+                    # Pandas uses datetime64[ns] dtype for dates.
+                    timestamp_dt = pa.timestamp("ns")
+                    if names and isinstance(parse_dates[0], str):
+                        # The `names` parameter could be used to override the
+                        # column names. If new names are specified in `parse_dates`
+                        # they should be replaced with the real names. Replacing
+                        # with the column indices first.
+                        parse_dates = [names.index(name) for name in parse_dates]
+                    if isinstance(parse_dates[0], int):
+                        # If column indices are specified, load the column names
+                        # with pandas and replace the indices with column names.
+                        column_names = get_col_names()
+                        parse_dates = [column_names[i] for i in parse_dates]
+                    for c in parse_dates:
+                        column_types[c] = timestamp_dt
+                elif not isinstance(parse_dates, bool):
+                    raise NotImplementedError(
+                        f"Argument parse_dates={parse_dates} is not supported"
+                    )
+
             sep = kwargs["sep"]
             delimiter = kwargs["delimiter"]
             if delimiter is None and sep is not lib.no_default:
@@ -198,25 +232,28 @@ def read_csv(cls, **kwargs):  # noqa: PR01
                 convert_options=co,
             )
 
-            col_names = at.column_names
-            col_counts = {}
-            for name in col_names:
-                col_counts[name] = 1 if name in col_counts else 0
-
-            if len(col_names) != len(col_counts):
-                for i, name in enumerate(col_names):
-                    count = col_counts[name]
-                    if count != 0:
-                        if count == 1:
-                            col_counts[name] = 2
-                        else:
-                            new_name = f"{name}.{count - 1}"
-                            while new_name in col_counts:
-                                new_name = f"{name}.{count}"
-                                count += 1
-                            col_counts[name] = count + 1
-                            col_names[i] = new_name
-                at = at.rename_columns(col_names)
+            if names:
+                at = at.rename_columns(names)
+            else:
+                col_names = at.column_names
+                col_counts = {}
+                for name in col_names:
+                    col_counts[name] = 1 if name in col_counts else 0
+
+                if len(col_names) != len(col_counts):
+                    for i, name in enumerate(col_names):
+                        count = col_counts[name]
+                        if count != 0:
+                            if count == 1:
+                                col_counts[name] = 2
+                            else:
+                                new_name = f"{name}.{count - 1}"
+                                while new_name in col_counts:
+                                    new_name = f"{name}.{count}"
+                                    count += 1
+                                col_counts[name] = count + 1
+                                col_names[i] = new_name
+                    at = at.rename_columns(col_names)
 
             return cls.from_arrow(at)
         except (
@@ -224,11 +261,14 @@ def read_csv(cls, **kwargs):  # noqa: PR01
             pa.ArrowInvalid,
             NotImplementedError,
             ArrowEngineException,
-        ):
+        ) as err:
             if eng in ["arrow"]:
                 raise
 
-            ErrorMessage.default_to_pandas("`read_csv`")
+            ErrorMessage.warn(
+                f"Failed to read csv {kwargs['filepath_or_buffer']} "
+                + f"due to error: {err}. Defaulting to pandas."
+            )
             return super().read_csv(**kwargs)
 
     @classmethod


Test patch:
diff --git a/modin/pandas/test/test_io.py b/modin/pandas/test/test_io.py
--- a/modin/pandas/test/test_io.py
+++ b/modin/pandas/test/test_io.py
@@ -546,15 +546,6 @@ def test_read_csv_datetime(
         dayfirst,
         cache_dates,
     ):
-        if (
-            StorageFormat.get() == "Hdk"
-            and isinstance(parse_dates, list)
-            and ("col4" in parse_dates or 3 in parse_dates)
-        ):
-            pytest.xfail(
-                "In some cases read_csv with `parse_dates` with HDK storage format outputs incorrect result - issue #3081"
-            )
-
         raising_exceptions = io_ops_bad_exc  # default value
         if isinstance(parse_dates, dict) and callable(date_parser):
             # In this case raised TypeError: <lambda>() takes 1 positional argument but 2 were given
@@ -575,6 +566,21 @@ def test_read_csv_datetime(
             cache_dates=cache_dates,
         )
 
+    @pytest.mark.parametrize("date", ["2023-01-01 00:00:01.000000000", "2023"])
+    @pytest.mark.parametrize("dtype", [None, "str", {"id": "int64"}])
+    @pytest.mark.parametrize("parse_dates", [None, [], ["date"], [1]])
+    def test_read_csv_dtype_parse_dates(self, date, dtype, parse_dates):
+        with ensure_clean(".csv") as filename:
+            with open(filename, "w") as file:
+                file.write(f"id,date\n1,{date}")
+            eval_io(
+                fn_name="read_csv",
+                # read_csv kwargs
+                filepath_or_buffer=filename,
+                dtype=dtype,
+                parse_dates=parse_dates,
+            )
+
     # Iteration tests
     @pytest.mark.parametrize("iterator", [True, False])
     def test_read_csv_iteration(self, iterator):