feat(harness): tht sql preview --json + --offset for AGGrid paging (BE-2)
Add inject_limit_offset (tht/execute/limit.py) — pure subquery wrapper that applies LIMIT/OFFSET non-destructively without clobbering user-supplied LIMITs. Wire offset param into do_run (pre-processing when offset>0) and add --offset / --json flags to preview_cmd; JSON mode emits pristine stdout with columns, rows, execution_ms, truncated, limit, offset. 5 new tests (4 unit + 1 JSON-purity), 219/219 total passing (no regressions). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""Task 6: `tht sql preview --json` + `--offset` paging.
|
||||
|
||||
Pure-logic tests (no DB needed):
|
||||
1. inject_limit_offset — subquery wrapping, non-destructive.
|
||||
2. preview_cmd JSON-purity — monkeypatched do_run, verifies stdout is valid JSON.
|
||||
"""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from tht.execute.limit import inject_limit_offset
|
||||
|
||||
|
||||
def test_inject_limit_offset_wraps_query():
|
||||
sql = "SELECT a FROM t ORDER BY a"
|
||||
out = inject_limit_offset(sql, limit=10, offset=20)
|
||||
assert "LIMIT 10" in out and "OFFSET 20" in out
|
||||
# original query stays as a subquery (no clobber of an existing user LIMIT)
|
||||
assert "SELECT a FROM t ORDER BY a" in out
|
||||
|
||||
|
||||
def test_inject_limit_offset_zero_offset_no_offset_clause():
|
||||
out = inject_limit_offset("SELECT 1", limit=5, offset=0)
|
||||
assert "LIMIT 5" in out
|
||||
assert "OFFSET" not in out
|
||||
|
||||
|
||||
def test_inject_limit_offset_strips_trailing_semicolon():
|
||||
out = inject_limit_offset("SELECT 1;", limit=3, offset=0)
|
||||
assert "LIMIT 3" in out
|
||||
# semicolon should not appear in the inner subquery
|
||||
assert ";" not in out
|
||||
|
||||
|
||||
def test_inject_limit_offset_default_offset_zero():
|
||||
out = inject_limit_offset("SELECT x FROM y", limit=7)
|
||||
assert "LIMIT 7" in out
|
||||
assert "OFFSET" not in out
|
||||
|
||||
|
||||
def test_preview_json_pure_stdout(monkeypatch, tmp_path, capsys):
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tht.cli import sql_cmd
|
||||
|
||||
fake = SimpleNamespace(columns=["a"], rows=[[1], [2]], execution_ms=3, truncated=False)
|
||||
monkeypatch.setattr(sql_cmd, "do_run", lambda *a, **k: fake)
|
||||
monkeypatch.setattr(sql_cmd, "validate_or_exit", lambda *a, **k: SimpleNamespace(ast=None))
|
||||
monkeypatch.setattr(sql_cmd, "require_action", lambda *a, **k: None)
|
||||
monkeypatch.setattr(
|
||||
sql_cmd,
|
||||
"_load_config_or_exit",
|
||||
lambda *a, **k: SimpleNamespace(execution=SimpleNamespace(max_preview_rows=100)),
|
||||
)
|
||||
f = tmp_path / "q.sql"
|
||||
f.write_text("SELECT 1")
|
||||
sql_cmd.preview_cmd(file=f, limit=None, offset=0, session=None, json_out=True, config=None)
|
||||
out = capsys.readouterr().out.strip()
|
||||
data = json.loads(out) # must parse: stdout pristine
|
||||
assert data["columns"] == ["a"]
|
||||
assert data["rows"] == [[1], [2]]
|
||||
assert data["execution_ms"] == 3
|
||||
assert data["truncated"] is False
|
||||
assert "limit" in data
|
||||
assert data["offset"] == 0
|
||||
Reference in New Issue
Block a user