Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,25 @@ def parse_json(content: str, record_path: Optional[str] = None, auto_explode: bo
if records is None:
return pd.DataFrame({'value': [data]})
if len(records) == 0:
return pd.DataFrame()
# Empty record array (e.g. {"results": [], "next_url": "..."}). A bare
# pd.DataFrame() is shape (0, 0) — zero columns — which DuckDB rejects
# downstream with "Need a DataFrame with at least one column" when the
# planner runs `SELECT * FROM df`. That surfaces to the agent as an opaque
# DuckDB error and triggers pointless query-rewrite retries, when the
# query was in fact valid and simply matched no rows.
#
# Preserve the envelope's top-level scalar siblings as columns so the
# empty result still carries metadata (status, count, next_url, ...);
# fall back to a single sentinel column so DuckDB always has a schema
# to describe.
columns = []
if isinstance(data, dict):
top_key = chosen_path.split('.')[0] if chosen_path else None
columns = [
k for k, v in data.items()
if k != top_key and (v is None or isinstance(v, (str, int, float, bool)))
]
return pd.DataFrame(columns=columns or ['_empty'])

df = pd.json_normalize(records, sep='.')

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,32 @@ def test_empty_array_returns_empty_dataframe():
df = parse_json(_dumps(payload))

assert len(df) == 0
# Must never be a (0, 0) frame: DuckDB's `SELECT * FROM df` requires at
# least one column, else it raises "Need a DataFrame with at least one
# column" and the agent misreads a valid empty result as a bad query.
assert df.shape[1] >= 1


def test_empty_array_preserves_envelope_scalar_columns():
# Regression: {"results": [], "next_url": "..."} previously produced a
# (0, 0) DataFrame, crashing DuckDB downstream. It must now yield an empty
# frame whose columns are the envelope's scalar siblings.
payload = {"results": [], "next_url": "https://api.example.com/next", "count": 0}
df = parse_json(_dumps(payload))

assert len(df) == 0
assert "next_url" in df.columns
assert "count" in df.columns
assert "results" not in df.columns


def test_empty_top_level_array_falls_back_to_sentinel_column():
# A bare empty list has no envelope metadata to borrow columns from, so it
# falls back to a sentinel column to keep DuckDB happy.
df = parse_json(_dumps([]))

assert len(df) == 0
assert df.shape[1] >= 1


def test_primitive_wrapped_in_value_column():
Expand Down
Loading