Repository: modin-project/modin

Issue:
BUG: String columns are converted to Categorical, if exported from HDK
### 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

df = pd.DataFrame({"A": ["A"]})
dtypes = df.dtypes
df = df[df["A"] != ""]
assert all(dtypes == df._to_pandas().dtypes)
```


### Issue Description

If an arrow table is exported from HDK, the string columns are converted to dictionary. On conversion to pandas, these columns will be of type Categorical, that is not correct.

### Expected Behavior

The columns types must not be changed to Categorical.

### 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/dataframe/dataframe.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/dataframe/dataframe.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/dataframe/dataframe.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/dataframe/dataframe.py
@@ -25,7 +25,12 @@
 from ..partitioning.partition_manager import HdkOnNativeDataframePartitionManager
 
 from pandas.core.indexes.api import ensure_index, Index, MultiIndex, RangeIndex
-from pandas.core.dtypes.common import get_dtype, is_list_like, is_bool_dtype
+from pandas.core.dtypes.common import (
+    get_dtype,
+    is_list_like,
+    is_bool_dtype,
+    is_categorical_dtype,
+)
 from modin.error_message import ErrorMessage
 from modin.pandas.indexing import is_range_like
 from modin.utils import MODIN_UNNAMED_SERIES_LABEL
@@ -60,6 +65,7 @@
 import pyarrow
 import re
 from modin.pandas.utils import check_both_not_none
+from pyarrow.types import is_dictionary
 
 
 class HdkOnNativeDataframe(PandasDataframe):
@@ -2288,7 +2294,28 @@ def to_pandas(self):
         if self._force_execution_mode == "lazy":
             raise RuntimeError("unexpected to_pandas triggered on lazy frame")
 
-        df = self._partition_mgr_cls.to_pandas(self._partitions)
+        if self._has_arrow_table():
+            # If the table is exported from HDK, the string columns are converted
+            # to dictionary. On conversion to pandas, these columns will be of type
+            # Categorical, that is not correct. To make the valid conversion, these
+            # fields are cast to string.
+            at = self._partitions[0][0].get()
+            schema = at.schema
+            cast = {
+                idx: arrow_type.name
+                for idx, (arrow_type, pandas_type) in enumerate(
+                    zip(schema, self._dtypes)
+                )
+                if is_dictionary(arrow_type.type)
+                and not is_categorical_dtype(pandas_type)
+            }
+            if cast:
+                for idx, new_type in cast.items():
+                    schema = schema.set(idx, pyarrow.field(new_type, pyarrow.string()))
+                at = at.cast(schema)
+            df = at.to_pandas()
+        else:
+            df = self._partition_mgr_cls.to_pandas(self._partitions)
 
         # If we make dataframe from Arrow table then we might need to set
         # index columns.
diff --git a/modin/experimental/sql/hdk/query.py b/modin/experimental/sql/hdk/query.py
--- a/modin/experimental/sql/hdk/query.py
+++ b/modin/experimental/sql/hdk/query.py
@@ -11,6 +11,9 @@
 # ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
+import pyarrow as pa
+from pandas.core.dtypes.common import get_dtype
+
 import modin.pandas as pd
 from modin.pandas.utils import from_arrow
 from modin.experimental.core.storage_formats.hdk import DFAlgQueryCompiler
@@ -66,7 +69,19 @@ def hdk_query(query: str, **kwargs) -> pd.DataFrame:
     worker = HdkWorker()
     if len(kwargs) > 0:
         query = _build_query(query, kwargs, worker.import_arrow_table)
-    return from_arrow(worker.executeDML(query))
+    df = from_arrow(worker.executeDML(query))
+    mdf = df._query_compiler._modin_frame
+    schema = mdf._partitions[0][0].get().schema
+    # HDK returns strings as dictionary. For the proper conversion to
+    # Pandas, we need to replace dtypes of the corresponding columns.
+    if replace := [
+        i for i, col in enumerate(schema) if pa.types.is_dictionary(col.type)
+    ]:
+        dtypes = mdf._dtypes
+        obj_type = get_dtype(object)
+        for i in replace:
+            dtypes[i] = obj_type
+    return df
 
 
 def _build_query(query: str, frames: dict, import_table: callable) -> str:


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
@@ -437,6 +437,14 @@ def filter(df, **kwargs):
 
         run_and_compare(filter, data=self.data)
 
+    def test_filter_str_categorical(self):
+        def filter(df, **kwargs):
+            return df[df["A"] != ""]
+
+        data = {"A": ["A", "B", "C"]}
+        run_and_compare(filter, data=data)
+        run_and_compare(filter, data=data, constructor_kwargs={"dtype": "category"})
+
 
 class TestMultiIndex:
     data = {"a": np.arange(24), "b": np.arange(24)}
diff --git a/modin/experimental/core/execution/native/implementations/hdk_on_native/test/utils.py b/modin/experimental/core/execution/native/implementations/hdk_on_native/test/utils.py
--- a/modin/experimental/core/execution/native/implementations/hdk_on_native/test/utils.py
+++ b/modin/experimental/core/execution/native/implementations/hdk_on_native/test/utils.py
@@ -316,24 +316,4 @@ def run_modin(
             constructor_kwargs=constructor_kwargs,
             **kwargs,
         )
-
-        # Currently, strings are converted to categories when exported from HDK,
-        # this makes the equality comparison fail. Converting string cols back to
-        # their original dtypes until the issue is resolved:
-        # https://github.com/modin-project/modin/issues/2747
-        if isinstance(exp_res, pd.DataFrame):
-            external_dtypes = exp_res.dtypes
-            exp_res = try_cast_to_pandas(exp_res)
-            internal_dtypes = exp_res.dtypes
-
-            new_schema = {}
-            for col in exp_res.columns:
-                if (
-                    not isinstance(internal_dtypes[col], pandas.Series)
-                    and internal_dtypes[col] == "category"
-                    and external_dtypes[col] != "category"
-                ):
-                    new_schema[col] = external_dtypes[col]
-            exp_res = exp_res.astype(new_schema)
-
         comparator(ref_res, exp_res)
diff --git a/modin/experimental/sql/test/test_sql.py b/modin/experimental/sql/test/test_sql.py
--- a/modin/experimental/sql/test/test_sql.py
+++ b/modin/experimental/sql/test/test_sql.py
@@ -11,9 +11,10 @@
 # ANY KIND, either express or implied. See the License for the specific language
 # governing permissions and limitations under the License.
 
+import pandas
 import modin.pandas as pd
 import modin.config as cfg
-from modin.pandas.test.utils import default_to_pandas_ignore_string
+from modin.pandas.test.utils import default_to_pandas_ignore_string, df_equals
 
 import io
 import pytest
@@ -71,3 +72,12 @@ def test_sql_extension():
     values_right = query_result.values
     assert values_left.shape == values_right.shape
     assert (values_left == values_right).all()
+
+
+def test_string_cast():
+    from modin.experimental.sql import query
+
+    data = {"A": ["A", "B", "C"], "B": ["A", "B", "C"]}
+    mdf = pd.DataFrame(data)
+    pdf = pandas.DataFrame(data)
+    df_equals(pdf, query("SELECT * FROM df", df=mdf))