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
4 changes: 3 additions & 1 deletion sqlit/domains/results/ui/mixins/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,12 +582,14 @@ 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]}..."

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
Expand Down
6 changes: 5 additions & 1 deletion sqlit/shared/ui/widgets_tables.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
60 changes: 60 additions & 0 deletions tests/ui/test_filtered_cell_tooltip.py
Original file line number Diff line number Diff line change
@@ -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
62 changes: 62 additions & 0 deletions tests/ui/test_uuid_fastdatatable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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"])

Expand Down Expand Up @@ -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")