Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/gooddata-sdk/src/gooddata_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@
ExecutionDefinition,
ExecutionResponse,
ExecutionResult,
ExecutionResultLimitBreak,
ResultCacheMetadata,
ResultSizeBytesLimitExceeded,
ResultSizeDimensions,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,30 @@
logger = logging.getLogger(__name__)


@define
class ExecutionResultLimitBreak:
"""Describes a limit that was broken, resulting in partial data being returned."""

limit: int
"""The configured threshold value."""

limit_type: str
"""Type of the limit that was broken, e.g. 'rowCount'."""

value: int | None = None
"""The actual value that triggered the limit; null when it cannot be determined exactly."""

@classmethod
def from_dict(cls, d: dict[str, Any]) -> ExecutionResultLimitBreak:
"""Construct from a raw API response dict (camelCase keys)."""
raw_value = d.get("value")
return cls(
limit=int(d["limit"]),
limit_type=str(d["limitType"]),
value=int(raw_value) if raw_value is not None else None,
)


@define
class TotalDimension:
idx: int
Expand Down Expand Up @@ -238,6 +262,12 @@ def __init__(self, result: models.ExecutionResult):
self._grand_totals: list[models.ExecutionResultGrandTotal] = result["grand_totals"]
self._paging: models.ExecutionResultPaging = result["paging"]
self._metadata: models.ExecutionResultMetadata = result["metadata"]
raw_limit_breaks = result.get("limitBreaks")
self._limit_breaks: list[ExecutionResultLimitBreak] = (
[ExecutionResultLimitBreak.from_dict(item) for item in raw_limit_breaks]
if raw_limit_breaks is not None
else []
)

@property
def data(self) -> list[Any]:
Expand Down Expand Up @@ -271,6 +301,14 @@ def paging_offset(self) -> list[int]:
def metadata(self) -> models.ExecutionResultMetadata:
return self._metadata

@property
def limit_breaks(self) -> list[ExecutionResultLimitBreak]:
"""Limits that were broken during result computation, causing the result to be partial.

Returns an empty list when the result is complete (no limits were broken).
"""
return self._limit_breaks

def is_complete(self, dim: int = 0) -> bool:
return self.paging_offset[dim] + self.paging_count[dim] >= self.paging_total[dim]

Expand Down
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from unittest.mock import MagicMock, patch

import pytest
from gooddata_sdk.compute.model.execution import ExecutionResultLimitBreak

pyarrow = pytest.importorskip("pyarrow")

Expand Down Expand Up @@ -104,3 +105,44 @@ def test_read_result_arrow_no_pyarrow_raises() -> None:

with patch.object(_exec_mod, "_ipc", None), pytest.raises(ImportError, match="pyarrow is required"):
bare.read_result_arrow()


@pytest.mark.parametrize(
"scenario, raw, expected_limit, expected_limit_type, expected_value",
[
(
"with_value",
{"limit": 1000, "limitType": "rowCount", "value": 1500},
1000,
"rowCount",
1500,
),
(
"value_none",
{"limit": 500, "limitType": "columnCount"},
500,
"columnCount",
None,
),
(
"value_explicit_none",
{"limit": 200, "limitType": "cellCount", "value": None},
200,
"cellCount",
None,
),
],
)
def test_execution_result_limit_break_from_dict(
scenario: str,
raw: dict,
expected_limit: int,
expected_limit_type: str,
expected_value: int | None,
) -> None:
"""ExecutionResultLimitBreak.from_dict correctly maps camelCase keys and handles optional value."""
lb = ExecutionResultLimitBreak.from_dict(raw)

assert lb.limit == expected_limit
assert lb.limit_type == expected_limit_type
assert lb.value == expected_value
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# (C) 2026 GoodData Corporation
from __future__ import annotations

from unittest.mock import MagicMock

from gooddata_sdk import ExecutionResultLimitBreak
from gooddata_sdk.compute.model.execution import ExecutionResult


def _make_mock_api_result(limit_breaks_value):
"""Return a mock that mimics a models.ExecutionResult dict-like object."""
_paging = {"total": [10, 1], "count": [10, 1], "offset": [0, 0]}
m = MagicMock()
m.__getitem__ = MagicMock(
side_effect=lambda k: {
"data": [],
"dimension_headers": [],
"grand_totals": [],
"paging": _paging,
"metadata": {},
}[k]
)
m.get = MagicMock(side_effect=lambda k, d=None: limit_breaks_value if k == "limitBreaks" else d)
return m


def test_execution_result_limit_breaks():
"""ExecutionResult.limit_breaks returns a list; non-empty when limits are broken.

For a normal execution the result is complete so limit_breaks must be [].
This test also verifies that the field is importable from gooddata_sdk and
that the ExecutionResultLimitBreak class is available on the public API.

Uses a synthetic mock result to avoid staging-server dependency.
"""
# --- Case 1: result where a row-count limit was broken ---
result = ExecutionResult(
_make_mock_api_result([{"limit": 1000, "limitType": "rowCount", "value": 1500}])
)
assert isinstance(result.limit_breaks, list)
assert len(result.limit_breaks) == 1
for lb in result.limit_breaks:
assert isinstance(lb, ExecutionResultLimitBreak)
assert isinstance(lb.limit, int)
assert isinstance(lb.limit_type, str)
assert lb.value is None or isinstance(lb.value, int)

# --- Case 2: complete result — no limits broken ---
result_complete = ExecutionResult(_make_mock_api_result(None))
assert isinstance(result_complete.limit_breaks, list)
assert result_complete.limit_breaks == []
Loading
Loading