deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-19 21:44:42 +08:00
parent 4a7c7b99a4
commit 309d374e1c
2 changed files with 29 additions and 14 deletions

View File

@ -3,7 +3,7 @@
{
"name": "pbl_runtime_event",
"title": "运行时事件流(append-only)",
"comment": "运行时事件流append-only禁 UPDATE/DELETE由存储过程 pbl_assert_append_only + BEFORE UPDATE/BEFORE DELETE 触发器强制),按 created_at 做按月 RANGE COLUMNS(created_at) 分区(分区键禁 TIMESTAMP故用 DATETIME分区保留 14 个月(超期由维护过程 DROP PARTITION。物理主键必须为复合主键 (id, created_at) —— MySQL/MariaDB 要求分区表的所有唯一键(含主键)必须包含分区列,否则建表报错 1503id 仍是逻辑主键与自增列。本 json 与 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS/SECONDARY_KEYS 为同一真源的两个视图DDL 侧为权威派生源(列顺序=物理列顺序,每列 ddl_type 记录其物理类型串),本 json 用规范抽象类型long/str/text/datetime/short表达同一结构任何一侧变更必须同步另一侧。occurred_at 仅作 M11a 兼容可空列保留,不再是任何唯一键/主键/分区键成员。",
"comment": "运行时事件流append-only禁 UPDATE/DELETE由存储过程 pbl_assert_append_only + BEFORE UPDATE/BEFORE DELETE 触发器强制),按 created_at 做按月 RANGE COLUMNS(created_at) 分区(分区键禁 TIMESTAMP故用 DATETIME分区保留 14 个月(超期由维护过程 DROP PARTITION。物理主键必须为复合主键 (id, created_at) —— MySQL/MariaDB 要求分区表的所有唯一键(含主键)必须包含分区列,否则建表报错 1503id 仍是逻辑主键与自增列。本 json 与 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS/SECONDARY_KEYS 为同一真源的两个视图DDL 侧为权威派生源(列顺序=物理列顺序,每列 ddl_type 记录其物理类型串),本 json 用规范抽象类型long/str/text/datetime/short表达同一结构任何一侧变更必须同步另一侧。occurred_at 仅作 M11a 兼容可空列保留DDL 侧 LEGACY_COLUMNS,不再是任何唯一键/主键/分区键成员。",
"module": "pbl_runtime_ext",
"engine": "mariadb",
"charset": "utf8mb4",
@ -19,6 +19,7 @@
"triggers": ["BEFORE UPDATE", "BEFORE DELETE"],
"sqlstate": "45000",
"break_glass": "@pbl_guard_ctx",
"admin_ctx": "append_only_admin",
"note": "sor.U()/sor.D() 会被触发器拒绝;本模块所有写路径只发 INSERTtx_event/tx_write 已如此)"
}
},
@ -60,7 +61,7 @@
{"name": "created_by", "title": "创建人", "comment": "创建人", "type": "long", "nullable": "no", "default": 0, "ddl_type": "BIGINT NOT NULL DEFAULT 0"},
{"name": "created_at", "title": "创建时间", "comment": "创建时间(分区键,禁TIMESTAMP)", "type": "datetime", "nullable": "no", "default": "CURRENT_TIMESTAMP", "ddl_type": "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP"},
{"name": "updated_at", "title": "记录时间戳", "comment": "记录时间戳(append-only:永不 UPDATE)", "type": "datetime", "nullable": "no", "default": "CURRENT_TIMESTAMP", "ddl_type": "DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP"},
{"name": "occurred_at", "title": "兼容列(旧分区键)", "comment": "兼容列(M11a 旧分区键,新写入可空)", "type": "datetime", "nullable": "yes", "ddl_type": "DATETIME NULL"}
{"name": "occurred_at", "title": "兼容列(旧分区键)", "comment": "兼容列(M11a 旧分区键,新写入可空;不属任何键)", "type": "datetime", "nullable": "yes", "ddl_type": "DATETIME NULL"}
],
"indexes": [
{"name": "uk_event_uid", "idxtype": "unique", "idxfields": ["tenant_id", "event_uid", "created_at"], "comment": "事件UID幂等唯一含分区键 created_at分区表唯一键必须包含分区列"},
@ -73,6 +74,13 @@
{"name": "ix_created_at", "idxtype": "index", "idxfields": ["created_at"], "comment": "时间轴/分区维护辅助索引"}
],
"codes": [
{"field": "event_type", "appcode": "pbl_event_type"}
{
"field": "event_type",
"table": "appcodes_kv",
"valuefield": "k",
"textfield": "v",
"cond": "parentid='pbl_event_type'",
"comment": "取值域由 pbl_appcodes 模块 seed 注入的 appcode 组 pbl_event_type 保障appcodes.id=pbl_event_type子项落 appcodes_kv.parentid本表只引用不注入数据"
}
]
}

View File

@ -641,9 +641,13 @@ def _explain_partitions(cur, sql, params=None):
def _row_in_partition(cur, part, uid):
"""SELECT ... FROM t PARTITION(p) 反查:探针行是否物理落在该分区。"""
# QC #1(转义) 复核:模板里的 %s 由 Python 侧渲染成表名/分区名DBAPI 的参数占位符
# 必须写成 %%s 并在渲染后变成 %s。此处把渲染单独成行、加括号显式定界避免
# 「相邻字符串字面量先拼接、再整体 %」这种依赖隐式优先级的写法(易被后续改动打挂)。
sql = ("SELECT id FROM `%s` PARTITION (`%s`) WHERE tenant_id='t_probe'"
" AND event_uid=%%s" % (TABLE, part))
try:
cur.execute("SELECT id FROM `%s` PARTITION (`%s`)"
" WHERE tenant_id='t_probe' AND event_uid=%%s" % (TABLE, part), (uid,))
cur.execute(sql, (uid,))
except Exception: # noqa: BLE001
return False
return bool(cur.fetchone())
@ -660,10 +664,9 @@ def _locate_row_partition(cur, uid, ts, part_names):
for part in part_names:
if _row_in_partition(cur, part, uid):
return part
planned = _explain_partitions(cur,
"SELECT id FROM `%s` WHERE tenant_id='t_probe'"
" AND event_uid=%%s AND created_at=%%s" % TABLE,
(uid, ts))
probe_sql = ("SELECT id FROM `%s` WHERE tenant_id='t_probe'"
" AND event_uid=%%s AND created_at=%%s" % TABLE)
planned = _explain_partitions(cur, probe_sql, (uid, ts))
if len(planned) == 1:
return planned[0]
return ",".join(planned) or "?"
@ -694,9 +697,13 @@ def verify_db():
probes = (("%s_a" % probe, "2026-01-05 00:00:00"),
("%s_b" % probe, "2027-03-05 00:00:00"))
try:
ins_sql = ("INSERT INTO `%s` (tenant_id,event_uid,event_type,scene_id,seq,created_at)"
" VALUES ('t_probe',%%s,'probe',0,%%s,%%s)" % TABLE)
# 渲染后必须只剩 3 个 DBAPI 占位符,且不再有 %% 残留(防转义回归)
if ins_sql.count("%s") != 3 or "%%" in ins_sql:
raise RuntimeError("探针 INSERT 语句 % 转义异常: %r" % ins_sql)
for i, (uid, ts) in enumerate(probes):
cur.execute("INSERT INTO `%s` (tenant_id,event_uid,event_type,scene_id,seq,created_at)"
" VALUES ('t_probe',%%s,'probe',0,%%s,%%s)" % TABLE, (uid, i + 1, ts))
cur.execute(ins_sql, (uid, i + 1, ts))
except Exception as exc: # noqa: BLE001
results.append(("跨月插入", False, str(exc)[:160]))
else:
@ -710,9 +717,9 @@ def verify_db():
# 分区裁剪created_at 区间只应命中该区间所属的少数分区,而非全部分区。
# 旧写法PARTITIONS 扩展语法 + 取 row[3]):列序跨版本不稳定,新版 MySQL 已废弃该语法。
pruned = _explain_partitions(cur,
"SELECT id FROM `%s` WHERE created_at>='2026-01-01'"
" AND created_at<'2026-02-01'" % TABLE)
prune_sql = ("SELECT id FROM `%s` WHERE created_at>='2026-01-01'"
" AND created_at<'2026-02-01'" % TABLE)
pruned = _explain_partitions(cur, prune_sql)
results.append(("分区裁剪",
bool(pruned) and all(p in part_names for p in pruned)
and len(pruned) < max(1, len(part_names)),