Files
AgentLab/backend/tests/jsonguard.py
T
ThearchyHelios 7febf2bcdb 两种库行为再对齐一轮(复核 NUL、R-a4、R-a6、R-a7、R-a10)
- 编排者的决定:PostgreSQL 下文本列写入时去掉 NUL 字符(before_cursor_execute 钩子,只装在应用库引擎上,看所有字符串参数,没有 NUL 的原样交回),SQLite 不变;同 JSON 列的处理
- R-a4 应用库地址里的 sslmode= 换成 asyncpg 的 ssl=(取值同一套),sslrootcert 等证书参数、application_name 这类 asyncpg 不收的参数启动时就报错并说怎么写(证书用 PGSSLROOTCERT 等环境变量);断点库连接串换回 sslmode=
- R-a6 不分大小写的搜索换成 db/portable.contains_ci:SQLite 上同 ilike(lower LIKE lower),PostgreSQL 上模式带 COLLATE "default"(列是 COLLATE "C",列上的 ILIKE 只认 ASCII,É/é、Ж/ж、全角A/a 匹配不上);运行列表按名字、对象索引按注释两处都改;改正 db/postgres 里那句说明
- R-a7 JSON 守卫改成编译层:tests/jsonguard 挂在应用库编译器上(conftest,两种库、跑全量时一直生效),整个 JSON 值出现在 = / != / IN / IS DISTINCT FROM / 大小比较、SELECT DISTINCT、GROUP BY、ORDER BY、UNION(去重的几种)里就报错;按类型认,变量别名、跨行 order_by、select(Run.origin).distinct()、JSON 下标的比较都抓得到,测试拿这些反例证明,json_eq、IS NULL、UNION ALL 不受影响。原来的逐行正则测试删掉
- R-a10 json_eq 两种库同一语义、按 JSON 类型比:字符串只等于字符串,数只等于数(1 等于 1.0),True / False 只等于 JSON 的 true / false(SQLite 上用 json_type 分开,以前 1 能匹配 true);非标量 TypeError。json_text 两种库都是「字符串取本身、别的取 JSON 写法」(SQLite 上同 PostgreSQL 的 ->>),数 1 的 json_text 两边都是 '1'
- db/postgres 说明里写清 NULL 排序补写的范围(只认直接按列排的)
2026-10-19 12:06:31 +08:00

64 lines
3.4 KiB
Python

"""测试时挂在应用库编译器上的守卫:整个 JSON 值做比较、去重、分组、排序就报错(两种库都挂,跑全量时一直生效)。
PostgreSQL 的 json 没有相等和大小的运算符:`=`、`!=`、`IN`、`IS DISTINCT FROM`、`<` 之类,SELECT DISTINCT、GROUP BY、
ORDER BY、UNION(去重的那几种)碰到 json 类型的值都报错;SQLite 照常执行。只在 PostgreSQL 上跑到才发现太晚:这里在编译
那一刻就拦下,SQLite 的测试也拦得到。按表达式的类型认,不按写法认:跨行的 order_by、变量别名、
`select(Run.origin).distinct()`、JSON 的下标(`Run.origin["k"] == 1`,取出来的还是 json)都算。按键筛选用 db/portable 的
json_eq 等(类型是布尔、文本),不受影响。
"""
from __future__ import annotations
from typing import Any
from sqlalchemy import JSON
from sqlalchemy.sql import operators
from sqlalchemy.sql.elements import UnaryExpression
class JsonMisuse(AssertionError):
"""整个 JSON 值被拿去比较、去重、分组或排序。"""
_COMPARE = {operators.eq, operators.ne, operators.in_op, operators.not_in_op, operators.is_distinct_from,
operators.is_not_distinct_from, operators.lt, operators.le, operators.gt, operators.ge}
def _json(element: Any) -> bool:
while isinstance(element, UnaryExpression) and element.modifier is not None:
element = element.element
return isinstance(getattr(element, "type", None), JSON)
class JsonGuard:
"""混进编译器类里用:class Guarded(JsonGuard, 原来的编译器)。"""
def visit_binary(self, binary: Any, **kw: Any) -> str:
if binary.operator in _COMPARE and (_json(binary.left) or _json(binary.right)):
raise JsonMisuse(f"整个 JSON 值做了比较({binary.operator.__name__}):PostgreSQL 的 json 没有这个运算符,"
"按键筛选用 db/portable.json_eq 等")
return super().visit_binary(binary, **kw) # type: ignore[misc]
def visit_select(self, select: Any, **kw: Any) -> str:
if select._distinct and any(_json(c) for c in select.selected_columns):
raise JsonMisuse("SELECT DISTINCT 里有 JSON 列:PostgreSQL 的 json 没有相等运算符,去不了重")
if any(_json(c) for c in select._group_by_clauses):
raise JsonMisuse("GROUP BY 了 JSON 值:PostgreSQL 的 json 没有相等运算符")
if any(_json(c) for c in select._order_by_clauses):
raise JsonMisuse("ORDER BY 了 JSON 值:PostgreSQL 的 json 没有大小")
return super().visit_select(select, **kw) # type: ignore[misc]
def visit_compound_select(self, cs: Any, **kw: Any) -> str:
keyword = str(getattr(cs.keyword, "value", cs.keyword)).upper()
if "ALL" not in keyword and any(_json(c) for c in cs.selected_columns):
raise JsonMisuse(f"{keyword or 'UNION'} 去重时碰到 JSON 列:PostgreSQL 的 json 没有相等运算符")
return super().visit_compound_select(cs, **kw) # type: ignore[misc]
def install(dialect: Any) -> None:
"""挂到应用库引擎的方言实例上(PostgreSQL 时是 db/postgres 换过的编译器,SQLite 时是原来的)。"""
base = dialect.statement_compiler
if issubclass(base, JsonGuard):
return
dialect.statement_compiler = type(f"JsonGuarded{base.__name__}", (JsonGuard, base), {})