deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
4a7c7b99a4
commit
309d374e1c
@ -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 要求分区表的所有唯一键(含主键)必须包含分区列,否则建表报错 1503;id 仍是逻辑主键与自增列。本 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 要求分区表的所有唯一键(含主键)必须包含分区列,否则建表报错 1503;id 仍是逻辑主键与自增列。本 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() 会被触发器拒绝;本模块所有写路径只发 INSERT(tx_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);本表只引用不注入数据"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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)),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user