From 4bad838aa20310b864ec7844e250eec213971170 Mon Sep 17 00:00:00 2001 From: Aaron Sykes Date: Tue, 8 Sep 2026 11:20:58 -0400 Subject: [PATCH 1/4] Render safe utf-8 values as text, fallback to rendering hex on error --- sqlit/shared/ui/widgets_tables.py | 6 ++- tests/ui/test_uuid_fastdatatable.py | 62 +++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/sqlit/shared/ui/widgets_tables.py b/sqlit/shared/ui/widgets_tables.py index cd617f31..ff4dcfc4 100644 --- a/sqlit/shared/ui/widgets_tables.py +++ b/sqlit/shared/ui/widgets_tables.py @@ -25,7 +25,11 @@ def normalize_arrow_value(value: Any) -> Any: if isinstance(value, UUID): return str(value) if isinstance(value, (bytes, bytearray, memoryview)): - return f"0x{bytes(value).hex()}" + raw = bytes(value) + try: + return raw.decode("utf-8") + except UnicodeDecodeError: + return f"0x{raw.hex()}" return value diff --git a/tests/ui/test_uuid_fastdatatable.py b/tests/ui/test_uuid_fastdatatable.py index cb829814..b1b74b9b 100644 --- a/tests/ui/test_uuid_fastdatatable.py +++ b/tests/ui/test_uuid_fastdatatable.py @@ -22,6 +22,8 @@ _INVALID_UTF8_BINARY = b"\x00\x00\x00\x00\x00\x00\x81\xff" _BINARY_TEXT = "0x00000000000081ff" +_UTF8_JSON_BINARY = b'{"a": 1, "b": [1, 2, 3]}' +_UTF8_JSON_TEXT = '{"a": 1, "b": [1, 2, 3]}' @pytest.mark.parametrize( @@ -44,6 +46,35 @@ def test_binary_column_is_stringified_before_arrow_measurement(value: object) -> assert table.get_cell_at(Coordinate(0, 0)) == _BINARY_TEXT +@pytest.mark.parametrize( + "value", + [ + _UTF8_JSON_BINARY, + bytearray(_UTF8_JSON_BINARY), + memoryview(_UTF8_JSON_BINARY), + ], + ids=["bytes", "bytearray", "memoryview"], +) +def test_utf8_binary_column_is_decoded_as_text(value: object) -> None: + table = SqlitDataTable( + data={"binary": [value]}, + column_labels=["binary"], + ) + + assert table.backend is not None + assert table.backend.column_content_widths == [len(_UTF8_JSON_TEXT)] + assert table.get_cell_at(Coordinate(0, 0)) == _UTF8_JSON_TEXT + + +def test_incrementally_added_utf8_binary_is_decoded_as_text() -> None: + table = SqlitDataTable(data={"binary": ["initial"]}, column_labels=["binary"]) + + table.add_rows([(_UTF8_JSON_BINARY,)]) + + assert table.backend is not None + assert table.get_cell_at(Coordinate(1, 0)) == _UTF8_JSON_TEXT + + def test_incrementally_added_binary_is_stringified() -> None: table = SqlitDataTable(data={"binary": ["initial"]}, column_labels=["binary"]) @@ -139,3 +170,34 @@ async def test_decimal_incremental_backend_stringifies_binary_column() -> None: _BINARY_TEXT ) assert app.results_table.get_cell_at(Coordinate(0, 2)) == _BINARY_TEXT + + +@pytest.mark.asyncio +async def test_view_cell_tooltip_does_not_parse_markup() -> None: + from rich.text import Text + + payload = b'{"path": "[/api/v1]", "tags": ["a", "b"]}' + connection = create_test_connection("test-db", "sqlite") + services = build_test_services( + connection_store=MockConnectionStore([connection]), + settings_store=MockSettingsStore({"theme": "tokyo-night"}), + ) + app = SSMSTUI(services=services) + + async with app.run_test(size=(120, 40)) as pilot: + await pilot.pause() + await app._display_query_results( + columns=["payload"], + rows=[(payload,)], + row_count=1, + truncated=False, + elapsed_ms=0, + ) + await pilot.pause(0.05) + app.results_table.cursor_coordinate = Coordinate(0, 0) + app.action_view_cell() + await pilot.pause(0.05) + + tooltip = app.results_table.tooltip + assert isinstance(tooltip, Text) + assert tooltip.plain == payload.decode("utf-8") From 2960dbbeb691bfad50b620887c8c9909c616a957 Mon Sep 17 00:00:00 2001 From: Aaron Sykes Date: Tue, 8 Sep 2026 11:21:23 -0400 Subject: [PATCH 2/4] Fix regression to crash on "v" preview for byte value fields. --- sqlit/domains/results/ui/mixins/results.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlit/domains/results/ui/mixins/results.py b/sqlit/domains/results/ui/mixins/results.py index 64b55ebb..af47badd 100644 --- a/sqlit/domains/results/ui/mixins/results.py +++ b/sqlit/domains/results/ui/mixins/results.py @@ -587,7 +587,8 @@ def _show_cell_tooltip( tooltip_value = f"{tooltip_value[:2000]}..." try: - table.tooltip = tooltip_value + # Wrap in Text so Rich does not parse cell content as markup. + table.tooltip = Text(tooltip_value) table._manual_tooltip_active = True except Exception: pass From 8ec734ef0d8f902ffe3146f48093a5207166fd9b Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:12:40 +0200 Subject: [PATCH 3/4] test(results): reproduce filtered cell tooltip markup regression --- tests/ui/test_filtered_cell_tooltip.py | 60 ++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/ui/test_filtered_cell_tooltip.py diff --git a/tests/ui/test_filtered_cell_tooltip.py b/tests/ui/test_filtered_cell_tooltip.py new file mode 100644 index 00000000..fde84a5c --- /dev/null +++ b/tests/ui/test_filtered_cell_tooltip.py @@ -0,0 +1,60 @@ +"""Cell previews must preserve values without leaking filter highlighting.""" + +from __future__ import annotations + +import pytest +from rich.text import Text +from textual.coordinate import Coordinate + +from sqlit.domains.shell.app.main import SSMSTUI + +from .mocks import ( + MockConnectionStore, + MockSettingsStore, + build_test_services, + create_test_connection, +) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("filtered", [False, True], ids=["unfiltered", "filtered"]) +@pytest.mark.parametrize( + "payload,search", + [("Jane", "Ja"), ('{"path": "[/api/v1]", "name": "Jane"}', "Jane")], + ids=["plain-text", "bracketed-json"], +) +async def test_cell_preview_preserves_original_value(payload: str, search: str, filtered: bool) -> None: + connection = create_test_connection("test-db", "sqlite") + services = build_test_services( + connection_store=MockConnectionStore([connection]), + settings_store=MockSettingsStore({"theme": "tokyo-night"}), + ) + app = SSMSTUI(services=services) + + async with app.run_test(size=(120, 40)) as pilot: + await pilot.pause() + await app._display_query_results( + columns=["payload"], rows=[(payload,)], row_count=1, + truncated=False, elapsed_ms=0, + ) + await pilot.pause() + app.results_table.focus() + if filtered: + await pilot.press("slash") + assert app._results_filter_visible + await pilot.press(*search) + await pilot.pause() + await pilot.press("enter") + await pilot.pause() + assert not app._results_filter_visible + assert app.results_table.render_markup + + app.results_table.cursor_coordinate = Coordinate(0, 0) + await pilot.press("v") + await pilot.pause() + assert app._tooltip_showing + tooltip = app.results_table.tooltip + assert tooltip is not None + # Textual renders string tooltips as markup, but Text values literally. + preview = tooltip.plain if isinstance(tooltip, Text) else Text.from_markup(tooltip).plain + assert preview == payload From 5d4bae60fe610999a3330cdf7abd58d776d0a5ca Mon Sep 17 00:00:00 2001 From: Peter Adams <18162810+Maxteabag@users.noreply.github.com> Date: Thu, 10 Sep 2026 05:16:08 +0200 Subject: [PATCH 4/4] fix(results): strip filter highlighting from cell previews --- sqlit/domains/results/ui/mixins/results.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlit/domains/results/ui/mixins/results.py b/sqlit/domains/results/ui/mixins/results.py index af47badd..c7b34568 100644 --- a/sqlit/domains/results/ui/mixins/results.py +++ b/sqlit/domains/results/ui/mixins/results.py @@ -582,6 +582,7 @@ def _show_cell_tooltip( self._tooltip_timer.stop() self._tooltip_timer = None + value = _strip_table_markup(table, value) tooltip_value = "NULL" if value is None else str(value) if len(tooltip_value) > 2000: tooltip_value = f"{tooltip_value[:2000]}..."