Repository: modin-project/modin

Issue:
Can't deserialize an empty Modin DataFrame using `read_parquet`
### System information
Modin 0.12.0
Ray 1.7.1
python 3.9.7
pyarrow 6.0.1


### Describe the problem
```
df = modin_pd.DataFrame([])
out = io.BytesIO()
df.to_parquet(path=out, engine="pyarrow", compression="snappy")

out.seek(0)
print(out.read())

pd.read_parquet(path=out, engine="pyarrow")
```

throws

```
b'PAR1\x15\x04\x15\x00\x15\x02L\x15\x00\x15\x04\x12\x00\x00\x00&&\x1c\x15\x02\x195\x04\x00\x06\x19\x18\x11__index_level_0__\x15\x02\x16\x00\x16\x1c\x16\x1e&\x00&\x08)\x1c\x15\x04\x15\x04\x15\x02\x00\x00\x00\x15\x02\x19,5\x00\x18\x06schema\x15\x02\x00\x15\x02%\x02\x18\x11__index_level_0__l\xbc\x00\x00\x00\x16\x00\x19\x1c\x19\x1c&&\x1c\x15\x02\x195\x04\x00\x06\x19\x18\x11__index_level_0__\x15\x02\x16\x00\x16\x1c\x16\x1e&\x00&\x08)\x1c\x15\x04\x15\x04\x15\x02\x00\x00\x00\x16\x1c\x16\x00&\x08\x16\x1e\x14\x00\x00\x19,\x18\x06pandas\x18\xf6\x02{"index_columns": ["__index_level_0__"], "column_indexes": [{"name": null, "field_name": null, "pandas_type": "empty", "numpy_type": "object", "metadata": null}], "columns": [{"name": null, "field_name": "__index_level_0__", "pandas_type": "empty", "numpy_type": "object", "metadata": null}], "creator": {"library": "pyarrow", "version": "6.0.1"}, "pandas_version": "1.3.4"}\x00\x18\x0cARROW:schema\x18\xec\x05/////ygCAAAQAAAAAAAKAA4ABgAFAAgACgAAAAABBAAQAAAAAAAKAAwAAAAEAAgACgAAAKwBAAAEAAAAAQAAAAwAAAAIAAwABAAIAAgAAACEAQAABAAAAHYBAAB7ImluZGV4X2NvbHVtbnMiOiBbIl9faW5kZXhfbGV2ZWxfMF9fIl0sICJjb2x1bW5faW5kZXhlcyI6IFt7Im5hbWUiOiBudWxsLCAiZmllbGRfbmFtZSI6IG51bGwsICJwYW5kYXNfdHlwZSI6ICJlbXB0eSIsICJudW1weV90eXBlIjogIm9iamVjdCIsICJtZXRhZGF0YSI6IG51bGx9XSwgImNvbHVtbnMiOiBbeyJuYW1lIjogbnVsbCwgImZpZWxkX25hbWUiOiAiX19pbmRleF9sZXZlbF8wX18iLCAicGFuZGFzX3R5cGUiOiAiZW1wdHkiLCAibnVtcHlfdHlwZSI6ICJvYmplY3QiLCAibWV0YWRhdGEiOiBudWxsfV0sICJjcmVhdG9yIjogeyJsaWJyYXJ5IjogInB5YXJyb3ciLCAidmVyc2lvbiI6ICI2LjAuMSJ9LCAicGFuZGFzX3ZlcnNpb24iOiAiMS4zLjQifQAABgAAAHBhbmRhcwAAAQAAABQAAAAQABQACAAGAAcADAAAABAAEAAAAAAAAQEQAAAAKAAAAAQAAAAAAAAAEQAAAF9faW5kZXhfbGV2ZWxfMF9fAAAABAAEAAQAAAA=\x00\x18\x1fparquet-cpp-arrow version 6.0.1\x19\x1c\x1c\x00\x00\x00\x1c\x05\x00\x00PAR1'
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/var/folders/9l/q_y0w5yx2_1_h65cprqgczyc0000gq/T/ipykernel_62102/810778694.py in <module>
      6 print(out.read())
      7 
----> 8 pd.read_parquet(path=out, engine="pyarrow")

lib/python3.9/site-packages/modin/pandas/io.py in read_parquet(path, engine, columns, storage_options, use_nullable_dtypes, **kwargs)
    214     Engine.subscribe(_update_engine)
    215     return DataFrame(
--> 216         query_compiler=FactoryDispatcher.read_parquet(
    217             path=path,
    218             engine=engine,

b/python3.9/site-packages/modin/core/execution/dispatching/factories/dispatcher.py in read_parquet(cls, **kwargs)
    173     @_inherit_docstrings(factories.BaseFactory._read_parquet)
    174     def read_parquet(cls, **kwargs):
--> 175         return cls.__factory._read_parquet(**kwargs)
    176 
    177     @classmethod

python3.9/site-packages/modin/core/execution/dispatching/factories/factories.py in _read_parquet(cls, **kwargs)
    195     )
    196     def _read_parquet(cls, **kwargs):
--> 197         return cls.io_cls.read_parquet(**kwargs)
    198 
    199     @classmethod

python3.9/site-packages/modin/core/io/file_dispatcher.py in read(cls, *args, **kwargs)
    149         postprocessing work on the resulting query_compiler object.
    150         """
--> 151         query_compiler = cls._read(*args, **kwargs)
    152         # TODO (devin-petersohn): Make this section more general for non-pandas kernel
    153         # implementations.

python3.9/site-packages/modin/core/io/column_stores/parquet_dispatcher.py in _read(cls, path, engine, columns, **kwargs)
    100                 column_names = [c for c in column_names if c not in index_columns]
    101             columns = [name for name in column_names if not PQ_INDEX_REGEX.match(name)]
--> 102         return cls.build_query_compiler(path, columns, **kwargs)

python3.9/site-packages/modin/core/io/column_stores/column_store_dispatcher.py in build_query_compiler(cls, path, columns, **kwargs)
    214             Query compiler with imported data for further processing.
    215         """
--> 216         col_partitions, column_widths = cls.build_columns(columns)
    217         partition_ids = cls.call_deploy(path, col_partitions, **kwargs)
    218         index, row_lens = cls.build_index(partition_ids)

python3.9/site-packages/modin/core/io/column_stores/column_store_dispatcher.py in build_columns(cls, columns)
    169         col_partitions = [
    170             columns[i : i + column_splits]
--> 171             for i in range(0, len(columns), column_splits)
    172         ]
    173         column_widths = [len(c) for c in col_partitions]

ValueError: range() arg 3 must not be zero
```


Discussion:
@c3-cjazra thank you for reporting the bug. I can reproduce it on my laptop.

Resolving patch:
diff --git a/modin/core/io/column_stores/column_store_dispatcher.py b/modin/core/io/column_stores/column_store_dispatcher.py
--- a/modin/core/io/column_stores/column_store_dispatcher.py
+++ b/modin/core/io/column_stores/column_store_dispatcher.py
@@ -122,7 +122,9 @@ def build_index(cls, partition_ids):
             List with lengths of index chunks.
         """
         num_partitions = NPartitions.get()
-        index_len = cls.materialize(partition_ids[-2][0])
+        index_len = (
+            0 if len(partition_ids) == 0 else cls.materialize(partition_ids[-2][0])
+        )
         if isinstance(index_len, int):
             index = pandas.RangeIndex(index_len)
         else:
@@ -158,15 +160,18 @@ def build_columns(cls, columns):
             List with lengths of `col_partitions` subarrays
             (number of columns that should be read by workers).
         """
+        columns_length = len(columns)
+        if columns_length == 0:
+            return [], []
         num_partitions = NPartitions.get()
         column_splits = (
-            len(columns) // num_partitions
-            if len(columns) % num_partitions == 0
-            else len(columns) // num_partitions + 1
+            columns_length // num_partitions
+            if columns_length % num_partitions == 0
+            else columns_length // num_partitions + 1
         )
         col_partitions = [
             columns[i : i + column_splits]
-            for i in range(0, len(columns), column_splits)
+            for i in range(0, columns_length, column_splits)
         ]
         column_widths = [len(c) for c in col_partitions]
         return col_partitions, column_widths
@@ -215,7 +220,11 @@ def build_query_compiler(cls, path, columns, **kwargs):
         partition_ids = cls.call_deploy(path, col_partitions, **kwargs)
         index, row_lens = cls.build_index(partition_ids)
         remote_parts = cls.build_partition(partition_ids[:-2], row_lens, column_widths)
-        dtypes = cls.build_dtypes(partition_ids[-1], columns)
+        dtypes = (
+            cls.build_dtypes(partition_ids[-1], columns)
+            if len(partition_ids) > 0
+            else None
+        )
         new_query_compiler = cls.query_compiler_cls(
             cls.frame_cls(
                 remote_parts,


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
@@ -1397,6 +1397,14 @@ def test_read_parquet_without_metadata(self):
         finally:
             teardown_test_files([parquet_fname, csv_fname])
 
+    def test_read_empty_parquet_file(self):
+        test_df = pandas.DataFrame()
+        with tempfile.TemporaryDirectory() as directory:
+            path = f"{directory}/data"
+            os.makedirs(path)
+            test_df.to_parquet(path + "/part-00000.parquet")
+            eval_io(fn_name="read_parquet", path=path)
+
     @pytest.mark.xfail(
         condition="config.getoption('--simulate-cloud').lower() != 'off'",
         reason="The reason of tests fail in `cloud` mode is unknown for now - issue #3264",