diff --git a/bigframes/core/blocks.py b/bigframes/core/blocks.py index 0f98f582c2..e140c49293 100644 --- a/bigframes/core/blocks.py +++ b/bigframes/core/blocks.py @@ -3471,6 +3471,28 @@ def _pd_index_to_array_value( Create an ArrayValue from a list of label tuples. The last column will be row offsets. """ + if index.empty: + id_gen = bigframes.core.identifiers.standard_id_strings() + col_ids = [next(id_gen) for _ in range(index.nlevels + 1)] + + data_dict = {} + if isinstance(index, pd.MultiIndex): + dtypes = index.dtypes.values.tolist() + else: + dtypes = [index.dtype] + + for col_id, dtype in zip(col_ids[:-1], dtypes): + try: + bf_dtype = bigframes.dtypes.bigframes_type(dtype) + pa_type = bigframes.dtypes.bigframes_dtype_to_arrow_dtype(bf_dtype) + except TypeError: + pa_type = pa.string() + data_dict[col_id] = pa.array([], type=pa_type) + + data_dict[col_ids[-1]] = pa.array([], type=pa.int64()) + table = pa.Table.from_pydict(data_dict) + return core.ArrayValue.from_pyarrow(table, session=session) + rows = [] labels_as_tuples = utils.index_as_tuples(index) for row_offset in range(len(index)): diff --git a/tests/unit/core/test_blocks_unpivot.py b/tests/unit/core/test_blocks_unpivot.py new file mode 100644 index 0000000000..f20a421146 --- /dev/null +++ b/tests/unit/core/test_blocks_unpivot.py @@ -0,0 +1,54 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import mock + +import pandas as pd +import pytest + +from bigframes.core import blocks + + +@pytest.fixture +def mock_session(): + session = mock.MagicMock() + session.bqclient = None + return session + + +def test_pd_index_to_array_value_with_empty_index_creates_columns(mock_session): + """ + Tests that `_pd_index_to_array_value` correctly handles an empty pandas Index by creating + an ArrayValue with the expected columns (index column + offset column). + This prevents crashes in `unpivot` which expects these columns to exist. + """ + empty_index = pd.Index([], name="test") + + array_val = blocks._pd_index_to_array_value(mock_session, empty_index) + + # Should be 2: one for index, one for offset + assert len(array_val.column_ids) == 2 + + +def test_pd_index_to_array_value_with_empty_multiindex_creates_columns(mock_session): + """ + Tests that `_pd_index_to_array_value` correctly handles an empty pandas MultiIndex by creating + an ArrayValue with the expected columns (one for each level + offset column). + """ + empty_index = pd.MultiIndex.from_arrays([[], []], names=["a", "b"]) + + array_val = blocks._pd_index_to_array_value(mock_session, empty_index) + + # Should have 3 columns: a, b, offset + assert len(array_val.column_ids) == 3