Skip to content
Open
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
20 changes: 19 additions & 1 deletion core/wren/src/wren/utils_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@
# _WARNING_SUMMARY_THRESHOLD so large batches don't flood stderr.
_SKIP_REPORT_LIMIT = 10

# Cap on the rendered length of a single listed value. Past this the repr is
# cut and marked, so a fragment is never mistaken for a complete value.
_VALUE_REPR_LIMIT = 120


def _format_corrupt_value(value: object) -> str:
"""Render ``value``'s repr, marking it when the length cap cuts it.

An unmarked cut reads exactly like a short value that happens to end
there, so a cut repr also reports the full length it was cut from.
"""
rendered = repr(value)
if len(rendered) <= _VALUE_REPR_LIMIT:
return rendered
return f"{rendered[:_VALUE_REPR_LIMIT]}... ({len(rendered)} chars total)"


def _skipped_rows(data: object) -> list[tuple[int, object]]:
"""Return (index, value) for entries parse_types/translate_types will drop.
Expand Down Expand Up @@ -53,7 +69,9 @@ def _report_skipped(skipped: list[tuple[int, object]]) -> None:
err=True,
)
for i, v in corrupt[:_SKIP_REPORT_LIMIT]:
typer.echo(f" [{i}] {type(v).__name__}: {v!r:.120}", err=True)
typer.echo(
f" [{i}] {type(v).__name__}: {_format_corrupt_value(v)}", err=True
)
remaining = len(corrupt) - _SKIP_REPORT_LIMIT
if remaining > 0:
typer.echo(f" ... and {remaining} more", err=True)
Expand Down
56 changes: 53 additions & 3 deletions core/wren/tests/unit/test_type_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
translate_type,
translate_types,
)
from wren.utils_cli import _VALUE_REPR_LIMIT

# ── parse_type unit tests ──────────────────────────────────────────────────

Expand Down Expand Up @@ -452,18 +453,49 @@ def test_cli_parse_types_skip_report_truncates_past_limit() -> None:
assert "Warning: skipped 12 non-mapping row(s)" in result.stderr
assert "[1] int: 0" in result.stderr
assert "... and 2 more" in result.stderr
# The trailing count is derived from _SKIP_REPORT_LIMIT, so it reads
# correctly even if the per-row listing was never capped. Pin the cap
# itself: row 11 is the first past the limit and must not be listed.
assert "[11] int: 10" not in result.stderr


def test_value_repr_limit_is_the_documented_cap() -> None:
# The cap is a display knob, but moving it changes user-visible output, so
# it should take a deliberate edit rather than drift silently. The
# rendering tests below derive their expectations from the constant, so
# this is the one place the value itself is pinned.
assert _VALUE_REPR_LIMIT == 120


def test_cli_parse_types_corrupt_value_repr_is_bounded() -> None:
# A few large corrupt values must not flood stderr just because there are
# fewer of them than _SKIP_REPORT_LIMIT.
columns = [{"column": "id", "raw_type": "int8"}, "x" * 5000]
# fewer of them than _SKIP_REPORT_LIMIT. The cut must also be visible, so a
# fragment is never read as the whole value.
raw = "x" * 5000
columns = [{"column": "id", "raw_type": "int8"}, raw]
result = _run_wren(
"utils", "parse-types", "--dialect", "postgres", stdin=json.dumps(columns)
)
_assert_success(result)
line = next(ln for ln in result.stderr.splitlines() if ln.startswith(" [1]"))
rendered = repr(raw)
assert line == (
f" [1] str: {rendered[:_VALUE_REPR_LIMIT]}... ({len(rendered)} chars total)"
)
Comment on lines 480 to +484

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 2cd15cf — pinned in one dedicated test (test_value_repr_limit_is_the_documented_cap) rather than hardcoded alongside each expectation, so the rendering tests stay correct at any cap while the value itself can no longer drift unnoticed. Verified both ways: changing the cap to 100 fails that test and nothing else.



def test_cli_parse_types_corrupt_value_repr_under_limit_is_unmarked() -> None:
# A value that fits the cap must render exactly as its repr, with no
# truncation marker to imply something was withheld.
raw = "short-but-wrong"
columns = [{"column": "id", "raw_type": "int8"}, raw]
result = _run_wren(
"utils", "parse-types", "--dialect", "postgres", stdin=json.dumps(columns)
)
_assert_success(result)
line = next(ln for ln in result.stderr.splitlines() if ln.startswith(" [1]"))
assert len(line) < 200
assert line == f" [1] str: {raw!r}"
assert "chars total" not in line


def test_cli_translate_types_strict_exits_nonzero_on_corrupt_row() -> None:
Expand Down Expand Up @@ -534,3 +566,21 @@ def test_cli_translate_types_rejects_non_list_object_payload() -> None:
assert result.returncode == 1
assert "Error: input must be a JSON array (got dict)" in result.stderr
assert "AssertionError" not in result.stderr


def test_cli_translate_types_rejects_non_list_string_payload() -> None:
# Mirrors the parse-types string-payload regression; both commands go
# through the same _require_list guard.
result = _run_wren(
"utils",
"translate-types",
"--source",
"postgres",
"--target",
"bigquery",
stdin=json.dumps("not-a-list"),
)
assert result.returncode == 1
assert "Error: input must be a JSON array (got str)" in result.stderr
Comment thread
coderabbitai[bot] marked this conversation as resolved.
assert "Traceback (most recent call last)" not in result.stderr
assert "AssertionError" not in result.stderr
Loading