ch: only a trailing FORMAT clause disables the JSONEachRow default
1 changed file +7 −2
modified
apps/api/src/internetpressure/db/ch.py
+7 −2
@@ -8,6 +8,7 @@ Usage: | |||||
| 8 | from __future__ import annotations | 8 | from __future__ import annotations | ||
| 9 | 9 | ||||
| 10 | import logging | 10 | import logging | ||
| 11 | +import re | ||||
| 11 | from pathlib import Path | 12 | from pathlib import Path | ||
| 12 | from typing import Any | 13 | from typing import Any | ||
| 13 | 14 | ||||
@@ -63,12 +64,16 @@ async def raw(sql: str, *, database: str | None = None, body: bytes | None = Non | |||||
| 63 | 64 | ||||
| 64 | 65 | ||||
| 65 | async def query(sql: str, params: dict | None = None) -> list[dict[str, Any]]: | 66 | async def query(sql: str, params: dict | None = None) -> list[dict[str, Any]]: | ||
| 66 | − if "FORMAT" not in sql.upper(): | 67 | + # only a trailing "FORMAT <name>" counts (an id like "plateforme" inside an IN (...) list must not match) | ||
| 68 | + if not re.search(r"\bFORMAT\s+\w+\s*;?\s*$", sql, re.IGNORECASE): | ||||
| 67 | sql = sql.rstrip().rstrip(";") + " FORMAT JSONEachRow" | 69 | sql = sql.rstrip().rstrip(";") + " FORMAT JSONEachRow" | ||
| 68 | out = await raw(sql, params=params) | 70 | out = await raw(sql, params=params) | ||
| 69 | if not out.strip(): | 71 | if not out.strip(): | ||
| 70 | return [] | 72 | return [] | ||
| 71 | − return [loads(line) for line in out.splitlines() if line.strip()] | 73 | + try: | ||
| 74 | + return [loads(line) for line in out.splitlines() if line.strip()] | ||||
| 75 | + except Exception as exc: # noqa: BLE001 — ClickHouse can emit an exception text after a 200 header was sent | ||||
| 76 | + raise CHError(f"ClickHouse returned non-JSON rows ({exc}): {out[:400]!r}\n-- {sql[:300]}") from exc | ||||
| 72 | 77 | ||||
| 73 | 78 | ||||
| 74 | async def query_one(sql: str, params: dict | None = None) -> dict[str, Any] | None: | 79 | async def query_one(sql: str, params: dict | None = None) -> dict[str, Any] | None: | ||
| 75 | 80 | ||||