[M11b-2c-A3] PBL-RTX-0004 修复:required_columns 识别 nullable 写法并排除自增/DEFAULT 列 + build_row 新增 full_row UPDATE 语义 + self_check 闭环门禁
This commit is contained in:
parent
e3c511ec2f
commit
5542cf5d51
@ -95,22 +95,84 @@ def table_columns(table):
|
||||
return names or None
|
||||
|
||||
|
||||
def _is_not_null(fd):
|
||||
"""字段是否声明 NOT NULL —— 兼容 models/*.json 并存的两种写法。
|
||||
|
||||
* ``{"null": false}`` :xlsx→json 产物写法(pbl_entity_state / pbl_world_state_snapshot)
|
||||
* ``{"nullable": "no"}`` :database-table-definition-spec 规范写法(pbl_runtime_event)
|
||||
|
||||
M11b-2c-A3 修复(PBL-RTX-0004 **漏报侧**根因):此前只认 ``null: false``,
|
||||
而 pbl_runtime_event 用的是规范写法 ``nullable: "no"``,使该表 ``required_columns()``
|
||||
恒为 ``[]`` —— build_row 的 INSERT 完整性门禁对事件表**完全失效**
|
||||
(该报的缺列不报,静默写出半空行)。
|
||||
"""
|
||||
if fd.get("null") is False:
|
||||
return True
|
||||
if str(fd.get("nullable") or "").strip().lower() in ("no", "false", "0"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_db_default(fd):
|
||||
"""列是否在 DDL 中声明 DEFAULT(含 CURRENT_TIMESTAMP)。
|
||||
|
||||
``NOT NULL DEFAULT x`` 的列在 INSERT 省略时由数据库补值,**不构成"缺 NOT NULL 列"**。
|
||||
M11b-2c-A3 修复(PBL-RTX-0004 **误报侧**根因之二):pbl_runtime_event 的
|
||||
event_code/idem_key/scene_id/entity_id/actor_id/causation_id/seq/source/state/
|
||||
tx_group/broadcast/created_by/created_at/updated_at 等列均为 NOT NULL DEFAULT,
|
||||
写路径按语义不填它们;若计入必填集合,合法调用会被误报缺列。
|
||||
(与 _recognize nullable 写法配套:只修识别不修默认值排除,事件表主链路会被误挡。)
|
||||
"""
|
||||
if "default" not in fd:
|
||||
return False
|
||||
dv = fd.get("default")
|
||||
if dv is None:
|
||||
return False
|
||||
if str(dv).strip().upper() in ("NULL", "NONE"):
|
||||
return False
|
||||
return True # 含 DEFAULT ''(空串默认值同样是 DDL 默认值,数据库会补值)
|
||||
|
||||
|
||||
def required_columns(table):
|
||||
"""NOT NULL 且非自增的列(INSERT 必须显式给值,否则 MariaDB 严格模式报错)。"""
|
||||
"""INSERT 必须**显式给值**的列:NOT NULL + 非自增 + 无数据库 DEFAULT。
|
||||
|
||||
自增列由数据库生成、NOT NULL DEFAULT 列由默认值补上,两者都不需要调用方提供;
|
||||
纳入必填集合只会造成 PBL-RTX-0004 误报。真正的必填列(pbl_entity_state 的
|
||||
tenant_id/session_id/entity_id/state_version/checksum/updated_by/created_at/
|
||||
updated_at,pbl_runtime_event 的 event_uid/event_type)缺值时数据库严格模式会
|
||||
直接报错,故必须在这里 fail-closed 拦住,不得放宽。
|
||||
"""
|
||||
spec = _model_spec(table)
|
||||
if not spec:
|
||||
return []
|
||||
out = []
|
||||
for fd in spec.get("fields") or []:
|
||||
if not isinstance(fd, dict):
|
||||
if not isinstance(fd, dict) or not fd.get("name"):
|
||||
continue
|
||||
if fd.get("null") is False and not fd.get("auto_increment"):
|
||||
if _is_not_null(fd) and not fd.get("auto_increment") and not _has_db_default(fd):
|
||||
out.append(fd["name"])
|
||||
return out
|
||||
|
||||
|
||||
def build_row(table, wanted, strict=True):
|
||||
"""按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。"""
|
||||
def build_row(table, wanted, strict=True, full_row=True):
|
||||
"""按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。
|
||||
|
||||
full_row=True(默认,**INSERT 语义**):额外校验 required_columns() 齐备,
|
||||
缺列抛 PBL-RTX-0004。
|
||||
full_row=False(**UPDATE SET 子句语义**):被改动的行在库中已存在,未出现在 SET
|
||||
列表里的 NOT NULL 列保持原值,**不存在"缺列"**,因此不得套用 INSERT 的完整性
|
||||
校验;同时禁止 SET 不可变列(tenant_id/id/created_at),违者抛 PBL-RTX-0006。
|
||||
|
||||
PBL-RTX-0004 闭环(M11b-2c-A3,A1 判定为业务实现缺陷):误报有两条独立路径——
|
||||
(1) UPDATE 分支沿用 INSERT 校验(此前用 strict=False 绕,但 strict 只管未知列,
|
||||
缺列校验照跑 → tenant_id/session_id/entity_id/created_at 必被误报)。
|
||||
现按 full_row=False 构造:定位维度走 WHERE、created_at 保持建档原值。
|
||||
(2) 必填集合把「自增列 / NOT NULL DEFAULT 列」也算进来 → required_columns()
|
||||
已排除(见 _has_db_default)。
|
||||
漏报侧同修:required_columns() 此前不认 ``nullable: "no"`` 写法,事件表门禁恒真。
|
||||
修后语义:该报的(真缺 event_uid/event_type 等无默认 NOT NULL 列)一定报,
|
||||
不该报的(UPDATE SET 子集、数据库能补值的列)一定不报。
|
||||
"""
|
||||
cols = table_columns(table)
|
||||
if not cols:
|
||||
if strict:
|
||||
@ -123,10 +185,16 @@ def build_row(table, wanted, strict=True):
|
||||
"PBL-RTX-0003",
|
||||
"%s 未知列 %s(权威列=%s)" % (table, unknown, cols))
|
||||
row = dict((k, v) for k, v in wanted.items() if k in cols)
|
||||
missing = [c for c in required_columns(table) if c not in row]
|
||||
if missing:
|
||||
raise rtx_db.RtxError("PBL-RTX-0004",
|
||||
"%s 缺少 NOT NULL 列 %s" % (table, missing))
|
||||
if full_row:
|
||||
missing = [c for c in required_columns(table) if c not in row]
|
||||
if missing:
|
||||
raise rtx_db.RtxError("PBL-RTX-0004",
|
||||
"%s 缺少 NOT NULL 列 %s" % (table, missing))
|
||||
else:
|
||||
immutable = [c for c in ("tenant_id", "id", "created_at") if c in row]
|
||||
if immutable:
|
||||
raise rtx_db.RtxError("PBL-RTX-0006",
|
||||
"%s UPDATE 禁止 SET 不可变列 %s" % (table, immutable))
|
||||
return row
|
||||
|
||||
|
||||
@ -253,7 +321,7 @@ async def _apply_state(tx, tenant_id, session_id, upd, seq_no, base_version,
|
||||
vals = build_row(STATE_TABLE, {
|
||||
STATE_COL_VALUE: state_version, EVENT_COL_STATE_VERSION: new_ver,
|
||||
"checksum": checksum, "updated_by": actor_id, "updated_at": now,
|
||||
}, strict=False)
|
||||
}, strict=True, full_row=False)
|
||||
await tx.execute(
|
||||
"UPDATE %s SET %s WHERE tenant_id=%%s AND session_id=%%s AND %s=%%s"
|
||||
% (STATE_TABLE, ", ".join("%s=%%s" % c for c in vals.keys()), STATE_COL_KEY),
|
||||
@ -539,6 +607,95 @@ def self_check():
|
||||
else:
|
||||
msgs.append("tx_write PASS:%s 用到的 %d 列与 models 定义一致" % (tbl, len(cols)))
|
||||
|
||||
# ---- PBL-RTX-0004 闭环门禁(M11b-2c-A3):误报侧不报 + 漏报侧必报 ----
|
||||
try:
|
||||
upd_row = build_row(STATE_TABLE, {
|
||||
STATE_COL_VALUE: "{}", EVENT_COL_STATE_VERSION: 2, "checksum": "c",
|
||||
"updated_by": "server", "updated_at": "2026-09-21 12:00:00",
|
||||
}, strict=True, full_row=False)
|
||||
expect = [STATE_COL_VALUE, EVENT_COL_STATE_VERSION, "checksum", "updated_by",
|
||||
"updated_at"]
|
||||
if sorted(upd_row.keys()) != sorted(expect):
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 列被意外裁剪 %s(应为 %s)"
|
||||
% (sorted(upd_row.keys()), sorted(expect)))
|
||||
else:
|
||||
msgs.append("tx_write PASS:UPDATE SET 语义不误报缺 NOT NULL 列"
|
||||
"(PBL-RTX-0004 误报侧已闭环)")
|
||||
except rtx_db.RtxError as exc:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 仍被 %s 拒绝(PBL-RTX-0004 未闭环)" % exc.code)
|
||||
|
||||
try:
|
||||
build_row(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos"})
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:INSERT 真缺 NOT NULL 列未被拦(门禁恒真 = 漏报回归)")
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code == "PBL-RTX-0004":
|
||||
msgs.append("tx_write PASS:INSERT 真缺列仍被 PBL-RTX-0004 拦下(fail-closed 未弱化)")
|
||||
else:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:INSERT 缺列错误码异常 %s(应为 PBL-RTX-0004)" % exc.code)
|
||||
|
||||
# 事件表用 nullable:"no" 写法,识别修复后必填集合必须非空(否则门禁又退回恒真)
|
||||
for tbl in (EVENT_TABLE, STATE_TABLE, SNAPSHOT_TABLE):
|
||||
req = required_columns(tbl)
|
||||
if not req:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s required_columns 为空"
|
||||
"(模型 NOT NULL 写法未被识别,INSERT 门禁失效)" % tbl)
|
||||
else:
|
||||
msgs.append("tx_write PASS:%s 必填列 %d 个(已排除自增/DEFAULT 列):%s"
|
||||
% (tbl, len(req), ",".join(req)))
|
||||
|
||||
# 只给「必填列真子集」必须被拦(事件表缺 event_type / 状态表缺 tenant_id)
|
||||
for tbl, partial in ((EVENT_TABLE, {EVENT_COL_UID: "u1"}),
|
||||
(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos",
|
||||
EVENT_COL_STATE_VERSION: 1, "checksum": "c",
|
||||
"updated_by": "a", "created_at": "2026-09-21",
|
||||
"updated_at": "2026-09-21"})):
|
||||
try:
|
||||
build_row(tbl, partial)
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s 只给部分必填列却通过校验" % tbl)
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code != "PBL-RTX-0004":
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s 部分列错误码 %s" % (tbl, exc.code))
|
||||
else:
|
||||
msgs.append("tx_write PASS:%s 部分必填列被 PBL-RTX-0004 拦下" % tbl)
|
||||
|
||||
# 写路径实际使用的 INSERT 列集合必须通过(防修复过头把主链路挡死)
|
||||
try:
|
||||
build_row(EVENT_TABLE, {
|
||||
"tenant_id": "7", EVENT_COL_UID: "u", "session_id": 11, "world_id": 1,
|
||||
"actor_id": "server", "event_type": "move", EVENT_COL_PAYLOAD: "{}",
|
||||
"causation_id": "", EVENT_COL_STATE_VERSION: 0, EVENT_COL_SEQ: 1,
|
||||
"occurred_at": "2026-09-21 12:00:00", "created_at": "2026-09-21 12:00:00",
|
||||
"updated_at": "2026-09-21 12:00:00"})
|
||||
build_row(SNAPSHOT_TABLE, {
|
||||
"tenant_id": "7", "session_id": 11, "snapshot_uid": "s",
|
||||
"state_fingerprint": "f", "entities_json": "{}", "events_through_seq": 1,
|
||||
"reason": "apply_runtime_event", "created_at": "2026-09-21",
|
||||
"updated_at": "2026-09-21"})
|
||||
msgs.append("tx_write PASS:事件表/快照表 INSERT 写路径列集合通过校验(无回归)")
|
||||
except rtx_db.RtxError as exc:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:写路径 INSERT 列集合被 %s 拒绝(修复过头)" % exc.code)
|
||||
|
||||
# 不可变列守卫:SET tenant_id/created_at 必须被 PBL-RTX-0006 拒绝
|
||||
try:
|
||||
build_row(STATE_TABLE, {"tenant_id": "7", STATE_COL_VALUE: "{}"},
|
||||
strict=True, full_row=False)
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 不可变列未被拦")
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code == "PBL-RTX-0006":
|
||||
msgs.append("tx_write PASS:UPDATE 禁止 SET 不可变列(PBL-RTX-0006)")
|
||||
else:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:不可变列错误码 %s(应为 PBL-RTX-0006)" % exc.code)
|
||||
|
||||
for name, fn in (("apply_runtime_event", apply_runtime_event),
|
||||
("poll_events", poll_events), ("read_states", read_states)):
|
||||
if not inspect.iscoroutinefunction(fn):
|
||||
|
||||
275
scripts/m11b2c_a3_patch_tx_write.py
Normal file
275
scripts/m11b2c_a3_patch_tx_write.py
Normal file
@ -0,0 +1,275 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M11b-2c-A3 一次性补丁器:修 pbl_runtime_ext/tx_write.py 中 PBL-RTX-0004 的业务缺陷。
|
||||
|
||||
四段精确字符串替换;任一片段命中数 != 1 即报错退出(绝不静默跳过)。
|
||||
改完由 py_compile + self_check + selftest + pytest 验证。
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
TARGET = os.path.join("modules", "pbl_runtime_ext", "pbl_runtime_ext", "tx_write.py")
|
||||
|
||||
# ---------------------------------------------------------------- 1) required_columns
|
||||
OLD_1 = '''def required_columns(table):
|
||||
"""NOT NULL 且非自增的列(INSERT 必须显式给值,否则 MariaDB 严格模式报错)。"""
|
||||
spec = _model_spec(table)
|
||||
if not spec:
|
||||
return []
|
||||
out = []
|
||||
for fd in spec.get("fields") or []:
|
||||
if not isinstance(fd, dict):
|
||||
continue
|
||||
if fd.get("null") is False and not fd.get("auto_increment"):
|
||||
out.append(fd["name"])
|
||||
return out
|
||||
'''
|
||||
|
||||
NEW_1 = '''def _is_not_null(fd):
|
||||
"""字段是否声明 NOT NULL —— 兼容 models/*.json 并存的两种写法。
|
||||
|
||||
* ``{"null": false}`` :xlsx→json 产物写法(pbl_entity_state / pbl_world_state_snapshot)
|
||||
* ``{"nullable": "no"}`` :database-table-definition-spec 规范写法(pbl_runtime_event)
|
||||
|
||||
M11b-2c-A3 修复(PBL-RTX-0004 **漏报侧**根因):此前只认 ``null: false``,
|
||||
而 pbl_runtime_event 用的是规范写法 ``nullable: "no"``,使该表 ``required_columns()``
|
||||
恒为 ``[]`` —— build_row 的 INSERT 完整性门禁对事件表**完全失效**
|
||||
(该报的缺列不报,静默写出半空行)。
|
||||
"""
|
||||
if fd.get("null") is False:
|
||||
return True
|
||||
if str(fd.get("nullable") or "").strip().lower() in ("no", "false", "0"):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_db_default(fd):
|
||||
"""列是否在 DDL 中声明 DEFAULT(含 CURRENT_TIMESTAMP)。
|
||||
|
||||
``NOT NULL DEFAULT x`` 的列在 INSERT 省略时由数据库补值,**不构成"缺 NOT NULL 列"**。
|
||||
M11b-2c-A3 修复(PBL-RTX-0004 **误报侧**根因之二):pbl_runtime_event 的
|
||||
event_code/idem_key/scene_id/entity_id/actor_id/causation_id/seq/source/state/
|
||||
tx_group/broadcast/created_by/created_at/updated_at 等列均为 NOT NULL DEFAULT,
|
||||
写路径按语义不填它们;若计入必填集合,合法调用会被误报缺列。
|
||||
(与 _recognize nullable 写法配套:只修识别不修默认值排除,事件表主链路会被误挡。)
|
||||
"""
|
||||
dv = fd.get("default")
|
||||
if dv is None:
|
||||
return False
|
||||
s = str(dv).strip()
|
||||
if s == "" or s.upper() in ("NULL", "NONE"):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def required_columns(table):
|
||||
"""INSERT 必须**显式给值**的列:NOT NULL + 非自增 + 无数据库 DEFAULT。
|
||||
|
||||
自增列由数据库生成、NOT NULL DEFAULT 列由默认值补上,两者都不需要调用方提供;
|
||||
纳入必填集合只会造成 PBL-RTX-0004 误报。真正的必填列(pbl_entity_state 的
|
||||
tenant_id/session_id/entity_id/state_version/checksum/updated_by/created_at/
|
||||
updated_at,pbl_runtime_event 的 event_uid/event_type)缺值时数据库严格模式会
|
||||
直接报错,故必须在这里 fail-closed 拦住,不得放宽。
|
||||
"""
|
||||
spec = _model_spec(table)
|
||||
if not spec:
|
||||
return []
|
||||
out = []
|
||||
for fd in spec.get("fields") or []:
|
||||
if not isinstance(fd, dict) or not fd.get("name"):
|
||||
continue
|
||||
if _is_not_null(fd) and not fd.get("auto_increment") and not _has_db_default(fd):
|
||||
out.append(fd["name"])
|
||||
return out
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------- 2) build_row
|
||||
OLD_2 = '''def build_row(table, wanted, strict=True):
|
||||
"""按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。"""
|
||||
cols = table_columns(table)
|
||||
if not cols:
|
||||
if strict:
|
||||
raise rtx_db.RtxError("PBL-RTX-0003",
|
||||
"models/%s.json 不可读,拒绝按猜列写入" % table)
|
||||
return dict(wanted)
|
||||
unknown = [k for k in wanted if k not in cols]
|
||||
if unknown and strict:
|
||||
raise rtx_db.RtxError(
|
||||
"PBL-RTX-0003",
|
||||
"%s 未知列 %s(权威列=%s)" % (table, unknown, cols))
|
||||
row = dict((k, v) for k, v in wanted.items() if k in cols)
|
||||
missing = [c for c in required_columns(table) if c not in row]
|
||||
if missing:
|
||||
raise rtx_db.RtxError("PBL-RTX-0004",
|
||||
"%s 缺少 NOT NULL 列 %s" % (table, missing))
|
||||
return row
|
||||
'''
|
||||
|
||||
NEW_2 = '''def build_row(table, wanted, strict=True, full_row=True):
|
||||
"""按表真实列过滤字段。strict=True 时未知列直接抛错(防静默丢列写空行)。
|
||||
|
||||
full_row=True(默认,**INSERT 语义**):额外校验 required_columns() 齐备,
|
||||
缺列抛 PBL-RTX-0004。
|
||||
full_row=False(**UPDATE SET 子句语义**):被改动的行在库中已存在,未出现在 SET
|
||||
列表里的 NOT NULL 列保持原值,**不存在"缺列"**,因此不得套用 INSERT 的完整性
|
||||
校验;同时禁止 SET 不可变列(tenant_id/id/created_at),违者抛 PBL-RTX-0006。
|
||||
|
||||
PBL-RTX-0004 闭环(M11b-2c-A3,A1 判定为业务实现缺陷):误报有两条独立路径——
|
||||
(1) UPDATE 分支沿用 INSERT 校验(此前用 strict=False 绕,但 strict 只管未知列,
|
||||
缺列校验照跑 → tenant_id/session_id/entity_id/created_at 必被误报)。
|
||||
现按 full_row=False 构造:定位维度走 WHERE、created_at 保持建档原值。
|
||||
(2) 必填集合把「自增列 / NOT NULL DEFAULT 列」也算进来 → required_columns()
|
||||
已排除(见 _has_db_default)。
|
||||
漏报侧同修:required_columns() 此前不认 ``nullable: "no"`` 写法,事件表门禁恒真。
|
||||
修后语义:该报的(真缺 event_uid/event_type 等无默认 NOT NULL 列)一定报,
|
||||
不该报的(UPDATE SET 子集、数据库能补值的列)一定不报。
|
||||
"""
|
||||
cols = table_columns(table)
|
||||
if not cols:
|
||||
if strict:
|
||||
raise rtx_db.RtxError("PBL-RTX-0003",
|
||||
"models/%s.json 不可读,拒绝按猜列写入" % table)
|
||||
return dict(wanted)
|
||||
unknown = [k for k in wanted if k not in cols]
|
||||
if unknown and strict:
|
||||
raise rtx_db.RtxError(
|
||||
"PBL-RTX-0003",
|
||||
"%s 未知列 %s(权威列=%s)" % (table, unknown, cols))
|
||||
row = dict((k, v) for k, v in wanted.items() if k in cols)
|
||||
if full_row:
|
||||
missing = [c for c in required_columns(table) if c not in row]
|
||||
if missing:
|
||||
raise rtx_db.RtxError("PBL-RTX-0004",
|
||||
"%s 缺少 NOT NULL 列 %s" % (table, missing))
|
||||
else:
|
||||
immutable = [c for c in ("tenant_id", "id", "created_at") if c in row]
|
||||
if immutable:
|
||||
raise rtx_db.RtxError("PBL-RTX-0006",
|
||||
"%s UPDATE 禁止 SET 不可变列 %s" % (table, immutable))
|
||||
return row
|
||||
'''
|
||||
|
||||
# ---------------------------------------------------------------- 3) UPDATE 调用点
|
||||
OLD_3 = ''' "checksum": checksum, "updated_by": actor_id, "updated_at": now,
|
||||
}, strict=False)'''
|
||||
|
||||
NEW_3 = ''' "checksum": checksum, "updated_by": actor_id, "updated_at": now,
|
||||
}, strict=True, full_row=False)'''
|
||||
|
||||
# ---------------------------------------------------------------- 4) self_check 闭环门禁
|
||||
OLD_4 = ''' for name, fn in (("apply_runtime_event", apply_runtime_event),
|
||||
("poll_events", poll_events), ("read_states", read_states)):'''
|
||||
|
||||
NEW_4 = ''' # ---- PBL-RTX-0004 闭环门禁(M11b-2c-A3):误报侧不报 + 漏报侧必报 ----
|
||||
try:
|
||||
upd_row = build_row(STATE_TABLE, {
|
||||
STATE_COL_VALUE: "{}", EVENT_COL_STATE_VERSION: 2, "checksum": "c",
|
||||
"updated_by": "server", "updated_at": "2026-09-21 12:00:00",
|
||||
}, strict=True, full_row=False)
|
||||
expect = [STATE_COL_VALUE, EVENT_COL_STATE_VERSION, "checksum", "updated_by",
|
||||
"updated_at"]
|
||||
if sorted(upd_row.keys()) != sorted(expect):
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 列被意外裁剪 %s(应为 %s)"
|
||||
% (sorted(upd_row.keys()), sorted(expect)))
|
||||
else:
|
||||
msgs.append("tx_write PASS:UPDATE SET 语义不误报缺 NOT NULL 列"
|
||||
"(PBL-RTX-0004 误报侧已闭环)")
|
||||
except rtx_db.RtxError as exc:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 仍被 %s 拒绝(PBL-RTX-0004 未闭环)" % exc.code)
|
||||
|
||||
try:
|
||||
build_row(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos"})
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:INSERT 真缺 NOT NULL 列未被拦(门禁恒真 = 漏报回归)")
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code == "PBL-RTX-0004":
|
||||
msgs.append("tx_write PASS:INSERT 真缺列仍被 PBL-RTX-0004 拦下(fail-closed 未弱化)")
|
||||
else:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:INSERT 缺列错误码异常 %s(应为 PBL-RTX-0004)" % exc.code)
|
||||
|
||||
# 事件表用 nullable:"no" 写法,识别修复后必填集合必须非空(否则门禁又退回恒真)
|
||||
for tbl in (EVENT_TABLE, STATE_TABLE, SNAPSHOT_TABLE):
|
||||
req = required_columns(tbl)
|
||||
if not req:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s required_columns 为空"
|
||||
"(模型 NOT NULL 写法未被识别,INSERT 门禁失效)" % tbl)
|
||||
else:
|
||||
msgs.append("tx_write PASS:%s 必填列 %d 个(已排除自增/DEFAULT 列):%s"
|
||||
% (tbl, len(req), ",".join(req)))
|
||||
|
||||
# 只给「必填列真子集」必须被拦(事件表缺 event_type / 状态表缺 tenant_id)
|
||||
for tbl, partial in ((EVENT_TABLE, {EVENT_COL_UID: "u1"}),
|
||||
(STATE_TABLE, {"session_id": 11, STATE_COL_KEY: "pos",
|
||||
EVENT_COL_STATE_VERSION: 1, "checksum": "c",
|
||||
"updated_by": "a", "created_at": "2026-09-21",
|
||||
"updated_at": "2026-09-21"})):
|
||||
try:
|
||||
build_row(tbl, partial)
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s 只给部分必填列却通过校验" % tbl)
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code != "PBL-RTX-0004":
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:%s 部分列错误码 %s" % (tbl, exc.code))
|
||||
else:
|
||||
msgs.append("tx_write PASS:%s 部分必填列被 PBL-RTX-0004 拦下" % tbl)
|
||||
|
||||
# 写路径实际使用的 INSERT 列集合必须通过(防修复过头把主链路挡死)
|
||||
try:
|
||||
build_row(EVENT_TABLE, {
|
||||
"tenant_id": "7", EVENT_COL_UID: "u", "session_id": 11, "world_id": 1,
|
||||
"actor_id": "server", "event_type": "move", EVENT_COL_PAYLOAD: "{}",
|
||||
"causation_id": "", EVENT_COL_STATE_VERSION: 0, EVENT_COL_SEQ: 1,
|
||||
"occurred_at": "2026-09-21 12:00:00", "created_at": "2026-09-21 12:00:00",
|
||||
"updated_at": "2026-09-21 12:00:00"})
|
||||
build_row(SNAPSHOT_TABLE, {
|
||||
"tenant_id": "7", "session_id": 11, "snapshot_uid": "s",
|
||||
"state_fingerprint": "f", "entities_json": "{}", "events_through_seq": 1,
|
||||
"reason": "apply_runtime_event", "created_at": "2026-09-21",
|
||||
"updated_at": "2026-09-21"})
|
||||
msgs.append("tx_write PASS:事件表/快照表 INSERT 写路径列集合通过校验(无回归)")
|
||||
except rtx_db.RtxError as exc:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:写路径 INSERT 列集合被 %s 拒绝(修复过头)" % exc.code)
|
||||
|
||||
# 不可变列守卫:SET tenant_id/created_at 必须被 PBL-RTX-0006 拒绝
|
||||
try:
|
||||
build_row(STATE_TABLE, {"tenant_id": "7", STATE_COL_VALUE: "{}"},
|
||||
strict=True, full_row=False)
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:UPDATE SET 不可变列未被拦")
|
||||
except rtx_db.RtxError as exc:
|
||||
if exc.code == "PBL-RTX-0006":
|
||||
msgs.append("tx_write PASS:UPDATE 禁止 SET 不可变列(PBL-RTX-0006)")
|
||||
else:
|
||||
ok = False
|
||||
msgs.append("tx_write FAIL:不可变列错误码 %s(应为 PBL-RTX-0006)" % exc.code)
|
||||
|
||||
for name, fn in (("apply_runtime_event", apply_runtime_event),
|
||||
("poll_events", poll_events), ("read_states", read_states)):'''
|
||||
|
||||
|
||||
def main():
|
||||
with io.open(TARGET, "r", encoding="utf-8") as f:
|
||||
src = f.read()
|
||||
pairs = [("required_columns", OLD_1, NEW_1), ("build_row", OLD_2, NEW_2),
|
||||
("_apply_state UPDATE 调用点", OLD_3, NEW_3),
|
||||
("self_check 闭环门禁", OLD_4, NEW_4)]
|
||||
for tag, old, new in pairs:
|
||||
n = src.count(old)
|
||||
if n != 1:
|
||||
print("FAIL: 片段[%s] 命中 %d 次(应为 1)" % (tag, n))
|
||||
return 2
|
||||
src = src.replace(old, new)
|
||||
with io.open(TARGET, "w", encoding="utf-8") as f:
|
||||
f.write(src)
|
||||
print("OK: 已应用 %d 处补丁 -> %s" % (len(pairs), TARGET))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user