@@ -1684,6 +1684,146 @@ def test_indexed_use_arrow_text_attr_unchanged() -> None:
16841684 assert all (isinstance (v , str ) for v in df .index .tolist ())
16851685
16861686
1687+ def test_indexed_use_arrow_empty_result_preserves_structure () -> None :
1688+ """indexed(use_arrow=True) on a zero-row Arrow table returns empty DataFrame with correct names."""
1689+ import json as _json
1690+
1691+ model_meta = {
1692+ "labels" : {"region" : {"granularity" : None , "labelTitle" : "Region" , "primaryLabelId" : "region" }},
1693+ "requestedShape" : {"metrics" : ["revenue" ]},
1694+ "metrics" : {"revenue" : {"title" : "Revenue" }},
1695+ }
1696+ xtab_meta = {
1697+ "labelMetadata" : {"l0" : {"labelId" : "region" , "primaryLabelId" : "region" }},
1698+ "computedShape" : {"rows" : [], "cols" : [], "metrics" : ["m0" ]},
1699+ "totalsMetadata" : {},
1700+ }
1701+ schema_meta = {
1702+ b"x-gdc-model-v1" : _json .dumps (model_meta ).encode (),
1703+ b"x-gdc-xtab-v1" : _json .dumps (xtab_meta ).encode (),
1704+ b"x-gdc-view-v1" : _json .dumps ({"isTransposed" : False }).encode (),
1705+ }
1706+ gdc_metric = {b"gdc" : _json .dumps ({"type" : "metric" , "index" : 0 }).encode ()}
1707+ schema = pa .schema (
1708+ [
1709+ pa .field ("__row_type" , pa .int8 ()),
1710+ pa .field ("region" , pa .string ()),
1711+ pa .field ("metric_group_0" , pa .float64 (), metadata = gdc_metric ),
1712+ ],
1713+ metadata = schema_meta ,
1714+ )
1715+ empty_table = pa .table (
1716+ {
1717+ "__row_type" : pa .array ([], type = pa .int8 ()),
1718+ "region" : pa .array ([], type = pa .string ()),
1719+ "metric_group_0" : pa .array ([], type = pa .float64 ()),
1720+ },
1721+ schema = schema ,
1722+ )
1723+
1724+ columns = {"revenue" : "metric/revenue" }
1725+ index_by = {"reg" : "label/region" }
1726+ mock_sdk , _ , _ = _mock_execution (empty_table , columns , index_by )
1727+
1728+ gdf = DataFrameFactory (mock_sdk , "workspace" , use_arrow = True )
1729+ df = gdf .indexed (index_by = index_by , columns = columns )
1730+
1731+ assert len (df ) == 0
1732+ assert list (df .columns ) == ["revenue" ]
1733+ assert df .index .name == "reg"
1734+
1735+
1736+ def test_extract_from_arrow_without_pyarrow_raises_import_error () -> None :
1737+ """_extract_from_arrow raises ImportError (not NameError) when pyarrow is unavailable."""
1738+ import gooddata_pandas .data_access as _da
1739+
1740+ original = _da ._ARROW_IMPORT_ERROR
1741+ try :
1742+ _da ._ARROW_IMPORT_ERROR = ImportError ("pyarrow not installed" )
1743+ with pytest .raises (ImportError , match = "pyarrow" ):
1744+ _da ._extract_from_arrow (MagicMock (), [], {}, {}, {})
1745+ finally :
1746+ _da ._ARROW_IMPORT_ERROR = original
1747+
1748+
1749+ def test_parse_schema_metadata_non_utf8_key_is_skipped () -> None :
1750+ """_parse_schema_metadata skips non-UTF-8 byte keys without raising UnicodeDecodeError."""
1751+ if "dim_r_m" not in _cases ():
1752+ pytest .skip ("fixture dim_r_m not available" )
1753+ table , _ , _ = _load_case ("dim_r_m" )
1754+ non_utf8_meta = {b"\xff \xfe " : b"some value" , ** table .schema .metadata }
1755+ table_with_bad_key = table .replace_schema_metadata (non_utf8_meta )
1756+ # Should not raise despite the non-UTF-8 key.
1757+ df = convert_arrow_table_to_dataframe (table_with_bad_key )
1758+ assert df is not None
1759+
1760+
1761+ def test_build_inline_index_total_row_numeric_label_uses_agg_name () -> None :
1762+ """Total rows with non-string (numeric/null) label values are replaced with the aggregation name."""
1763+ table = pa .table (
1764+ {
1765+ "__row_type" : pa .array ([0 , 2 ], type = pa .int8 ()),
1766+ "year" : pa .array ([2023.0 , None ], type = pa .float64 ()),
1767+ # Data row has no total ref; total row refers to "t0" in totalsMetadata.
1768+ "__total_ref" : pa .array ([None , [0 ]], type = pa .list_ (pa .int32 ())),
1769+ }
1770+ )
1771+ xtab_meta = {
1772+ "labelMetadata" : {"l0" : {"labelId" : "year" , "primaryLabelId" : "year" }},
1773+ "totalsMetadata" : {"t0" : {"aggregation" : "sum" , "rowLabels" : []}},
1774+ }
1775+ model_meta = {
1776+ "labels" : {"year" : {"labelTitle" : "Year" }},
1777+ "requestedShape" : {"metrics" : []},
1778+ }
1779+ idx = _build_inline_index (
1780+ table ,
1781+ row_label_refs = ["l0" ],
1782+ label_ref_to_id = {"l0" : "year" },
1783+ model_meta = model_meta ,
1784+ xtab_meta = xtab_meta ,
1785+ )
1786+ assert idx is not None
1787+ assert idx [0 ] == 2023.0
1788+ assert idx [1 ] == "SUM"
1789+
1790+
1791+ def test_arrow_config_max_bytes_forwarded_to_read_result_arrow () -> None :
1792+ """ArrowConfig.max_bytes is passed through to read_result_arrow() by for_exec_def_arrow()."""
1793+ if "dim_r_m" not in _cases ():
1794+ pytest .skip ("fixture dim_r_m not available" )
1795+ table , _ , meta = _load_case ("dim_r_m" )
1796+
1797+ mock_exec = MagicMock ()
1798+ mock_exec .bare_exec_response .read_result_arrow .return_value = table
1799+ mock_exec .bare_exec_response .dimensions = meta ["dimensions" ]
1800+ mock_sdk = MagicMock ()
1801+ mock_sdk .compute .for_exec_def .return_value = mock_exec
1802+
1803+ from gooddata_sdk import ExecutionDefinition
1804+
1805+ gdf = DataFrameFactory (mock_sdk , "workspace" , arrow_config = ArrowConfig (max_bytes = 10_000_000 ))
1806+ gdf .for_exec_def_arrow (MagicMock (spec = ExecutionDefinition ))
1807+
1808+ mock_exec .bare_exec_response .read_result_arrow .assert_called_once_with (max_bytes = 10_000_000 )
1809+
1810+
1811+ def test_arrow_config_max_bytes_raises_when_exceeded () -> None :
1812+ """ResultSizeBytesLimitExceeded from read_result_arrow propagates out of for_exec_def_arrow()."""
1813+ from gooddata_sdk .compute .model .execution import ResultSizeBytesLimitExceeded
1814+
1815+ mock_exec = MagicMock ()
1816+ mock_exec .bare_exec_response .read_result_arrow .side_effect = ResultSizeBytesLimitExceeded (100 , 200 )
1817+ mock_sdk = MagicMock ()
1818+ mock_sdk .compute .for_exec_def .return_value = mock_exec
1819+
1820+ from gooddata_sdk import ExecutionDefinition
1821+
1822+ gdf = DataFrameFactory (mock_sdk , "workspace" , arrow_config = ArrowConfig (max_bytes = 100 ))
1823+ with pytest .raises (ResultSizeBytesLimitExceeded ):
1824+ gdf .for_exec_def_arrow (MagicMock (spec = ExecutionDefinition ))
1825+
1826+
16871827def test_indexed_use_arrow_mixed_date_and_text_index () -> None :
16881828 """indexed() with use_arrow=True: date attr → Timestamp, text attr → str in MultiIndex."""
16891829 n = 3
0 commit comments