2026-09-19 11:43:35 +08:00

239 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
"""pbl_runtime_ext 表定义与幂等建表M11a4 表,单一真源)。
4 表mariadb 方言BIGINT AUTO_INCREMENT 主键tenant_id 强制打头,无 FK/ENUM/TIMESTAMP
* pbl_runtime_event —— 运行时事件流append-onlyidem_key 幂等唯一键)
* pbl_entity_state —— 实体状态服务端权威state_version 客户端禁写)
* pbl_world_state_snapshot —— 世界状态快照(单事务写入后广播基线)
* pbl_runtime_idempotency —— client_seq 幂等闸门M11a 新增,唯一键
(tenant_id, session_id, scope, client_seq)
**单一真源**:本文件的 DDL 列表是唯一权威定义,`sql/pbl_runtime_ext.sql` 与
`models/*.json` 由 `scripts/gen_artifacts.py` 从本文件生成,三处不可能漂移。
表名常量TBL_*)供代码引用,禁止在业务代码里再写裸表名字符串。
"""
TBL_RUNTIME_EVENT = "pbl_runtime_event"
TBL_ENTITY_STATE = "pbl_entity_state"
TBL_SNAPSHOT = "pbl_world_state_snapshot"
TBL_IDEMPOTENCY = "pbl_runtime_idempotency"
TABLES = [TBL_RUNTIME_EVENT, TBL_ENTITY_STATE, TBL_SNAPSHOT, TBL_IDEMPOTENCY]
# 表名常量 → 幂等闸门/状态权威链路用到的关键列(供自检与文档核对,避免列名漂移)
IDEMPOTENCY_COLUMNS = ["idempotency_id", "tenant_id", "session_id", "scope", "client_seq",
"principal_id", "request_fp", "state", "result_json", "error_code",
"created_at", "updated_at"]
ENTITY_STATE_VERSION_COLUMN = "state_version"
DDL = [
"""
CREATE TABLE IF NOT EXISTS `pbl_runtime_event` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`event_code` VARCHAR(64) NOT NULL COMMENT '事件编码',
`idem_key` VARCHAR(128) NOT NULL DEFAULT '' COMMENT '幂等键(同键重复投递只落一条)',
`world_id` BIGINT NOT NULL DEFAULT 0 COMMENT '世界ID引用 world 基表,只读)',
`session_id` BIGINT NOT NULL DEFAULT 0 COMMENT '游戏会话ID引用 scense 基表,只读)',
`entity_id` BIGINT NOT NULL DEFAULT 0 COMMENT '实体ID引用 entity 基表,只读)',
`event_type` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '事件类型',
`payload` TEXT COMMENT '事件负载 JSON',
`seq_no` BIGINT NOT NULL DEFAULT 0 COMMENT '会话内单调序号',
`source` VARCHAR(32) NOT NULL DEFAULT 'runtime' COMMENT '来源runtime/agent/script',
`state` VARCHAR(16) NOT NULL DEFAULT 'applied' COMMENT 'applied/rejected/rolled_back',
`tx_group` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '事务组(同组同事务)',
`broadcast` TINYINT NOT NULL DEFAULT 1 COMMENT '是否已广播 0/1',
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_idem` (`tenant_id`, `idem_key`),
KEY `ix_tenant_session_seq` (`tenant_id`, `session_id`, `seq_no`),
KEY `ix_tenant_world` (`tenant_id`, `world_id`),
KEY `ix_tenant_type` (`tenant_id`, `event_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运行时事件流append-only'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_entity_state` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`state_code` VARCHAR(64) NOT NULL COMMENT '状态编码',
`world_id` BIGINT NOT NULL DEFAULT 0 COMMENT '世界ID',
`session_id` BIGINT NOT NULL DEFAULT 0 COMMENT '会话ID',
`entity_id` BIGINT NOT NULL DEFAULT 0 COMMENT '实体ID引用 entity 基表,只读)',
`state_key` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '状态键',
`state_value` TEXT COMMENT '状态值 JSON',
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT '服务端权威单调版本(客户端禁写)',
`last_event_id` BIGINT NOT NULL DEFAULT 0 COMMENT '最后触发事件ID',
`created_at` DATETIME COMMENT '创建时间',
`updated_at` DATETIME COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_entity_key` (`tenant_id`, `session_id`, `entity_id`, `state_key`),
KEY `ix_tenant_world` (`tenant_id`, `world_id`),
KEY `ix_tenant_state_version` (`tenant_id`, `session_id`, `state_version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实体状态薄扩展服务端权威state_version 客户端禁写)'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_world_state_snapshot` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`snapshot_code` VARCHAR(64) NOT NULL COMMENT '快照编码',
`world_id` BIGINT NOT NULL DEFAULT 0 COMMENT '世界ID',
`session_id` BIGINT NOT NULL DEFAULT 0 COMMENT '会话ID',
`seq_no` BIGINT NOT NULL DEFAULT 0 COMMENT '快照对应事件序号',
`state` TEXT COMMENT '世界状态 JSON',
`entity_count` INT NOT NULL DEFAULT 0 COMMENT '实体数',
`checksum` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '状态校验和',
`created_by` BIGINT NOT NULL DEFAULT 0 COMMENT '创建人',
`created_at` DATETIME COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_snapshot_code` (`tenant_id`, `snapshot_code`),
KEY `ix_tenant_session_seq` (`tenant_id`, `session_id`, `seq_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='世界状态快照'
""",
"""
CREATE TABLE IF NOT EXISTS `pbl_runtime_idempotency` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
`tenant_id` BIGINT NOT NULL DEFAULT 0 COMMENT '租户ID多租户强制打头',
`idempotency_id` VARCHAR(64) NOT NULL COMMENT '幂等记录业务主键',
`session_id` BIGINT NOT NULL DEFAULT 0 COMMENT '会话ID引用 scense 基表,只读)',
`scope` VARCHAR(64) NOT NULL DEFAULT '*' COMMENT '幂等作用域world_id 或 world_id:channel缺省 *',
`client_seq` BIGINT NOT NULL DEFAULT 0 COMMENT '客户端单调序号(>=1',
`principal_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '提交者(服务端会话解析)',
`request_fp` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '请求体指纹(同 seq 不同内容判冲突)',
`state` VARCHAR(16) NOT NULL DEFAULT 'in_progress' COMMENT 'in_progress/completed/failed',
`result_json` TEXT COMMENT '已完成结果缓存(重放直接返回,不再写第二条事件)',
`error_code` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '失败错误码failed 态)',
`updated_at` BIGINT NOT NULL DEFAULT 0 COMMENT '更新时间epoch 毫秒)',
`created_at` BIGINT NOT NULL DEFAULT 0 COMMENT '创建时间epoch 毫秒)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_seq` (`tenant_id`, `session_id`, `scope`, `client_seq`),
UNIQUE KEY `uk_tenant_idem_id` (`tenant_id`, `idempotency_id`),
KEY `ix_tenant_scope_seq` (`tenant_id`, `session_id`, `scope`, `state`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运行时 client_seq 幂等闸门M11a 服务端权威)'
""",
]
def table_names():
"""全部表名list[str])。"""
return list(TABLES)
def get_ddl():
"""返回本模块全部建表语句list[str])。"""
return [x.strip() for x in DDL]
def ddl_for(table):
"""按表名取建表语句;未知表名抛 KeyErrorfail-closed不静默返回空"""
for stmt in get_ddl():
if stmt.split("`")[1] == table:
return stmt
raise KeyError("未知表 %s(应为 %s 之一)" % (table, ",".join(TABLES)))
def columns_for(table):
"""按表名解析列名清单(从 DDL 单一真源解析,供 gen_artifacts / 自检使用)。"""
cols = []
for line in ddl_for(table).splitlines():
s = line.strip()
if s.startswith("`") and s.endswith(","):
cols.append(s.split("`")[1])
return cols
def ensure_tables(env=None, sor=None):
"""幂等建表CREATE TABLE IF NOT EXISTS。返回 (ok_list, bad_list)。"""
ok, bad = [], []
runner = sor
if runner is None and env is not None:
runner = getattr(env, "sor", None) or getattr(env, "db", None)
for stmt in get_ddl():
name = stmt.split("`")[1] if "`" in stmt else "?"
try:
if runner is not None and hasattr(runner, "sqlExe"):
runner.sqlExe(stmt)
elif env is not None and hasattr(env, "sqlExe"):
env.sqlExe(stmt)
else:
if "AUTO_INCREMENT" not in stmt or "tenant_id" not in stmt:
raise ValueError("DDL 形态不合规:%s" % name)
ok.append(name)
except Exception as exc: # noqa: BLE001
bad.append((name, str(exc)[:160]))
return ok, bad
def self_check():
"""离线自检4 表齐全 / tenant_id 打头 / 无禁用方言 / 幂等唯一键在位 / 列名与代码一致。"""
msgs = []
all_ok = True
forbidden = ("BIGSERIAL", "SERIAL", "nextval", "FOREIGN KEY",
"REFERENCES", "ENUM(", "TIMESTAMP")
seen = []
for stmt in get_ddl():
name = stmt.split("`")[1]
seen.append(name)
body = stmt.upper()
if name not in TABLES:
all_ok = False
msgs.append("未知表 %s" % name)
for kw in forbidden:
if kw in body:
all_ok = False
msgs.append("%s 命中禁用方言 %s" % (name, kw))
cols = [ln.strip().split("`")[1] for ln in stmt.splitlines() if ln.strip().startswith("`")]
biz = [c for c in cols if c != "id"]
if not biz or biz[0] != "tenant_id":
all_ok = False
msgs.append("%s 首个业务列=%s(应为 tenant_id" % (name, biz[:1]))
if "AUTO_INCREMENT" not in body:
all_ok = False
msgs.append("%s 缺 AUTO_INCREMENT 主键" % name)
if "IF NOT EXISTS" not in body:
all_ok = False
msgs.append("%s 非幂等建表(缺 IF NOT EXISTS" % name)
if sorted(seen) != sorted(TABLES):
all_ok = False
msgs.append("DDL 表集合 %s 与 TABLES %s 不一致" % (sorted(seen), sorted(TABLES)))
# 事件表幂等唯一键
if "UNIQUE KEY `uk_tenant_idem`" not in ddl_for(TBL_RUNTIME_EVENT):
all_ok = False
msgs.append("pbl_runtime_event 缺幂等唯一键 uk_tenant_idem")
else:
msgs.append("pbl_runtime_event uk_tenant_idem 在位")
# 状态表:服务端权威版本列必须是 state_version代码引用的真实列名
state_cols = columns_for(TBL_ENTITY_STATE)
if ENTITY_STATE_VERSION_COLUMN not in state_cols:
all_ok = False
msgs.append("pbl_entity_state 缺列 %s(代码 MAX(state_version) 会 Unknown column"
% ENTITY_STATE_VERSION_COLUMN)
else:
msgs.append("pbl_entity_state.%s 在位(服务端权威,客户端禁写)" % ENTITY_STATE_VERSION_COLUMN)
if "version" in state_cols and ENTITY_STATE_VERSION_COLUMN not in state_cols:
all_ok = False
msgs.append("pbl_entity_state 仍用旧列名 version应统一为 state_version")
# 幂等闸门表:列 + 唯一键
idem_cols = columns_for(TBL_IDEMPOTENCY)
for col in IDEMPOTENCY_COLUMNS:
if col not in idem_cols:
all_ok = False
msgs.append("pbl_runtime_idempotency 缺列 %sidempotency.py 读写依赖)" % col)
idem_ddl = ddl_for(TBL_IDEMPOTENCY)
if "UNIQUE KEY `uk_tenant_seq` (`tenant_id`, `session_id`, `scope`, `client_seq`)" not in idem_ddl:
all_ok = False
msgs.append("pbl_runtime_idempotency 缺唯一键 uk_tenant_seq(tenant_id,session_id,scope,client_seq)")
if all_ok and set(IDEMPOTENCY_COLUMNS) <= set(idem_cols):
msgs.append("pbl_runtime_idempotency 列/唯一键齐全(%d 列)" % len(idem_cols))
if len(TABLES) != 4:
all_ok = False
msgs.append("表数=%d 应为 4" % len(TABLES))
if all_ok:
msgs.append("SELF_CHECK pbl_runtime_ext.tables: PASS %d/%d" % (len(TABLES), len(TABLES)))
return all_ok, msgs