perf(api): хелпер ответа из готовых JSON-строк и SQL-выражение строки

Добавлены list_json_response (обёртка BaseListResponse из готовых строк),
ROW_COLUMNS, row_json_select и fetch_row_texts для тяжёлых GET (AURORA-1848).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-28 21:07:40 +04:00
parent 0175759f95
commit 7094fe839f
3 changed files with 72 additions and 0 deletions

View File

@ -0,0 +1,14 @@
"""Ответ тяжёлых GET из готовых JSON-строк Postgres (AURORA-1848, design D3)."""
from fastapi import Response
def list_json_response(rows: list[str], headers: dict[str, str] | None = None) -> Response:
"""Обёртка как у BaseListResponse при успехе; rows — уже сериализованные строки ответа."""
body = b"".join((
b'{"success":true,"message":null,"result":[',
",".join(rows).encode(),
b'],"count":',
str(len(rows)).encode(),
b"}",
))
return Response(content=body, media_type="application/json", headers=headers)

View File

@ -0,0 +1,17 @@
"""Строка ответа тяжёлого GET как текст JSON из Postgres (AURORA-1848, design D1)."""
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
ROW_COLUMNS = "row_type, depth, sort_order, data"
def row_json_select(row_type: str = "row_type", depth: str = "depth") -> str:
"""json (не jsonb) сохраняет порядок ключей; ::text обходит кодек json.loads у asyncpg."""
return (
f"SELECT json_build_object('row_type', {row_type}, 'depth', {depth}, "
f"'data', data, 'sort_order', sort_order)::text"
)
async def fetch_row_texts(db: AsyncSession, sql: str, params: dict) -> list[str]:
return list((await db.execute(text(sql), params)).scalars().all())

View File

@ -0,0 +1,41 @@
import json
from src.api.v1.json_response import list_json_response
from src.repository.row_json import ROW_COLUMNS, row_json_select
def test_list_json_response_empty():
r = list_json_response([])
assert json.loads(r.body) == {"success": True, "message": None, "result": [], "count": 0}
def test_list_json_response_rows_order_count_escaping():
rows = [
'{"row_type":"META","depth":-1,"data":{"a":null},"sort_order":null}',
r'{"row_type": "INPUT", "depth": 3, "data": {"t": "Кавычка \" и \\ слэш\nперенос"}, "sort_order": 1}',
]
body = json.loads(list_json_response(rows).body)
assert list(body) == ["success", "message", "result", "count"]
assert body["count"] == 2
assert [row["row_type"] for row in body["result"]] == ["META", "INPUT"]
assert body["result"][0]["data"] == {"a": None}
assert body["result"][1]["data"]["t"] == 'Кавычка " и \\ слэш\nперенос'
def test_list_json_response_media_type_and_headers():
r = list_json_response(["{}"], headers={"X-DB-Time-Ms": "1.50"})
assert r.media_type == "application/json"
assert r.headers["content-type"] == "application/json"
assert r.headers["x-db-time-ms"] == "1.50"
assert r.headers["content-length"] == str(len(r.body))
def test_row_json_select_key_order():
assert ROW_COLUMNS == "row_type, depth, sort_order, data"
assert row_json_select() == (
"SELECT json_build_object('row_type', row_type, 'depth', depth, "
"'data', data, 'sort_order', sort_order)::text"
)
assert row_json_select(row_type="'INPUT'::VARCHAR", depth="3").startswith(
"SELECT json_build_object('row_type', 'INPUT'::VARCHAR, 'depth', 3, "
)