Repository: scikit-hep/awkward

Issue:
Schema is not preserved when reading an empty `RecordArray` from parquet
### Version of Awkward Array

2.0.6

### Description and code to reproduce

Hi!

I ran into what looks like a bug (or rather a non-implemented feature) when reading an `ak.RecordArray` stored in a Parquet file. The schema/type is not preserved when converting back to awkward if the stored array has no content (all columns have length zero).

This happens occasionally in a distributed workflow I'm running that involves merging a large number of arrays with the same structure (some of which could be empty) stored across multiple Parquet files. Preserving the schema even for the empty ones would make this easier to automate without having to filter out the empty arrays beforehand.

Here's a minimal example with an assertion on the array type to check schema preservation:
```python
import awkward as ak

# create a RecordArray with some content
a = ak.zip({"field_1": [1, 2], "field_2": [3, 4]})

# get an empty array (preserve schema)
a_empty = a[a['field_1'] < 0]

# write and read parquet
ak.to_parquet(a_empty, "test_empty.parquet")
a_empty_from_parquet = ak.from_parquet("test_empty.parquet")  # raises NotImplementedError

# check schema preserved
assert (a_empty_from_parquet.type == a_empty.type)
```

The `from_parquet` line raises a `NotImplemented` error:
```
Traceback (most recent call last):
  File "/.../awkward/test_parquet_empty.py", line 11, in <module>
    a_empty_from_parquet = ak.from_parquet("test_empty.parquet")  # gives NotImplementedError
  File "/.../awkward/src/awkward/operations/ak_from_parquet.py", line 75, in from_parquet
    return _load(
  File "/.../awkward/src/awkward/operations/ak_from_parquet.py", line 224, in _load
    _read_parquet_file(
  File "/.../awkward/src/awkward/operations/ak_from_parquet.py", line 304, in _read_parquet_file
    return ak.operations.ak_from_arrow._impl(
  File "/.../awkward/src/awkward/operations/ak_from_arrow.py", line 51, in _impl
    out = awkward._connect.pyarrow.handle_arrow(
  File "/.../awkward/src/awkward/_connect/pyarrow.py", line 1005, in handle_arrow
    raise ak._errors.wrap_error(NotImplementedError)
NotImplementedError:
```

This doesn't happen when round-tripping the array through Arrow, so it seems to be Parquet-related:
```python
a_empty_via_arrow = ak.from_arrow(ak.to_arrow(a_empty))  # works as expected
assert (a_empty_via_arrow.type == a_empty.type)
```


Discussion:

Resolving patch:
diff --git a/src/awkward/_connect/pyarrow.py b/src/awkward/_connect/pyarrow.py
--- a/src/awkward/_connect/pyarrow.py
+++ b/src/awkward/_connect/pyarrow.py
@@ -1001,8 +1001,11 @@ def handle_arrow(obj, generate_bitmasks=False, pass_empty_field=False):
     elif isinstance(obj, pyarrow.lib.Table):
         batches = obj.combine_chunks().to_batches()
         if len(batches) == 0:
-            # FIXME: create a zero-length array with the right type
-            raise ak._errors.wrap_error(NotImplementedError)
+            # create an empty array following the input schema
+            return form_handle_arrow(
+                obj.schema,
+                pass_empty_field=pass_empty_field,
+            ).length_zero_array(highlevel=False)
         elif len(batches) == 1:
             return handle_arrow(batches[0], generate_bitmasks, pass_empty_field)
         else:


Test patch:
diff --git a/tests/test_1125_to_arrow_from_arrow.py b/tests/test_1125_to_arrow_from_arrow.py
--- a/tests/test_1125_to_arrow_from_arrow.py
+++ b/tests/test_1125_to_arrow_from_arrow.py
@@ -695,3 +695,11 @@ def test_unionarray(tmp_path, extensionarray):
     )
     paarray = akarray.to_arrow(extensionarray=extensionarray)
     arrow_round_trip(akarray, paarray, extensionarray)
+
+
+@pytest.mark.parametrize("extensionarray", [False, True])
+def test_empty_arrow(tmp_path, extensionarray):
+    akarray = ak.Array([{"x": 1, "y": 2.2}])[0:0]
+    paarray = ak.to_arrow_table(akarray, extensionarray=extensionarray)
+    arrow_round_trip(akarray, paarray, extensionarray)
+    parquet_round_trip(akarray, paarray, extensionarray, tmp_path)