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

This commit is contained in:
agent.develop 2026-09-20 20:29:07 +08:00
parent fc359f8dc5
commit ff5f881d51
3 changed files with 176 additions and 222 deletions

View File

@ -5,7 +5,7 @@
"title": "运行时事件流(append-only)",
"primary": ["id", "created_at"],
"catelog": "entity",
"comment": "【本表定位 · world_sync 侧内联副本】运行时事件流append-only禁 UPDATE/DELETE由权威模块 pbl_runtime_ext 的存储过程 pbl_assert_append_only + BEFORE UPDATE/BEFORE DELETE 触发器强制;按 created_at 做按月 RANGE COLUMNS(created_at) 分区,分区保留 14 个月)。物理主键为复合主键 (id, created_at) —— MySQL/MariaDB 要求分区表的所有唯一键(含主键)必须包含分区列,否则建表报错 1503。【结构权威源 · PM 裁决】PM 裁决(任务 u0EO4x3ajntR41acaHj5H 问答):选 B —— 以 pbl_runtime_ext 为唯一真源;结构权威定义位于 modules/pbl_runtime_ext/models/pbl_runtime_event.jsonM11b-1 已批准产物,本任务禁止反向修改)DDL 派生源为该模块 scripts/pbl_runtime_event_ddl.py 与 scripts/migrations/*.sql建表/分区/触发器归 pbl_runtime_ext 所有本文件不复制那套逻辑。【为什么是内联而不是引用】database-table-definition-spec 只承认 summary/fields/indexes/codes 四段式scripts/validate_models_json.pyM11b-2a 子任务 A 修复版)对 key=schema 引用式写法直接判 FAILQC #5故本文件按真源逐字段内联权威列集作为 world_sync 侧的消费方契约视图;两份定义的一致性由 scripts/m11b2b_column_diff.py 与 scripts/m11b2b_selftest.py 机械核对真源列集变更时必须同步本副本。【id 类型偏离处理 · 本子任务的选择与理由】真源 id 为 type=long + auto_incrementBIGINT AUTO_INCREMENT真源侧已在其 summary[0].comment 自行登记该偏离);子任务 A 的最终登记表 COMPLIANCE_DEVIATIONS 为空 {}A 按 QC #5 选 (a):不放行任何历史失效项),且本任务范围明确禁止修改校验器脚本,因此 world_sync 侧不再走「登记放行 id=long」路线而是内联为 spec 合规写法 type=str / length=32、不带 auto_increment 键,由应用层生成 32 位 uuid 去横线十六进制串作为主键值(见 world_sync/pbl_runtime_tx.py::new_event_uid正是 uuid4().hex 32 位)。与真源的 id 类型差异由 world_sync 侧消费方代码适配:写路径 INSERT 不显式提交 id 时交由真源 DDL 的 AUTO_INCREMENT 生成BIGINT本地/测试库scripts 渲染的 sqlite DDL则使用应用层生成的 str(32);对外幂等与跨系统追溯一律使用 event_uidstr 64uk_event_uid 唯一键),不依赖 id 的物理类型。此差异为有意选择、非疏漏评审请勿据此判为结构缺陷。【消费方清单】modules/world_sync/world_sync/pbl_runtime_sql.pyEVENT_TABLE/EVENT_COLUMNS、modules/world_sync/world_sync/pbl_runtime_tx.py::write_event_with_state单事务事件+状态写入、modules/world_sync/world_sync/pbl_runtime_authority.py列契约解析层上述代码引用的列名必须全部出现在本文件 fields[] 中。【打包镜像说明】apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 是本文件的打包镜像,只由 modules/world_sync/scripts/sync_models_mirror.py 生成,禁止手工双写;一致性由 modules/world_sync/scripts/m11b2b_selftest.py 的 sha256 断言守护。【私有键剥离说明】真源使用的 spec 未定义顶层扩展键 module/engine/charset/collate/tenant_scoped/append_only/write_paths/guards/partition/ddl_source以及 fields[].ddl_type/comment/auto_increment、indexes[].comment、codes[].comment在本副本中一律不保留(本文件根键仅 summary/fields/indexes/codessummary[0] 仅 name/title/primary/catelog/commentfields[] 仅 name/title/type/length/dec/nullable/defaultindexes[] 仅 name/idxtype/idxfieldscodes[] 仅 field/table/valuefield/textfield/cond);这些物理实现细节以真源与其 DDL 派生源为准。"
"comment": "【本表定位】world_sync 侧消费方契约副本运行时事件流append-only禁 UPDATE/DELETE生产库由权威模块 pbl_runtime_ext 的存储过程 pbl_assert_append_only + BEFORE UPDATE/BEFORE DELETE 触发器强制,本副本只声明列契约、不复制那套物理实现)。【结构权威源 · PM 裁决】PM 裁决(任务 u0EO4x3ajntR41acaHj5H选 B modules/pbl_runtime_ext/models/pbl_runtime_event.json 为唯一真源M11b-1 已批准产物,本任务禁止反向修改);建表 DDL 派生源为该模块 scripts/pbl_runtime_event_ddl.py 与 scripts/migrations/*.sql。本文件是按真源逐字段内联的 world_sync 侧副本25 列全量抄录),不是引用式 schema —— database-table-definition-spec 只承认 summary/fields/indexes/codes 四段式scripts/validate_models_json.py 对 key=schema 引用式写法直接判 FAILQC #5故 QC 要求内联。两份定义的一致性由 scripts/m11b2b_column_diff.py(代码列名 vs 权威列集双向 diff与 scripts/m11b2b_selftest.py(四段式/抽象类型/主键/索引/编码 6 项断言 + 真源镜像 sha256机械核对真源列集变更时必须同步本副本。【id 生成方 · 单一口径QC #4】本副本声明 id 由应用层生成world_sync/pbl_runtime_tx.py::new_event_uid()uuid4().hex32 位小写十六进制),写路径 INSERT 列集(= 本文件 fields[] 全列,见 pbl_runtime_sql.py::EVENT_COLUMNS 由 fields[] 机械导出)显式提交 id不依赖任何数据库 AUTO_INCREMENT。真源侧 id 为 type=long + auto_incrementBIGINT AUTO_INCREMENT属其私有物理实现本副本按 spec 归一为 str(32) 且不保留 auto_increment 键;两者差异已在交付件正文登记为 DEVIATION(WS-2) 并冒泡 agent.pm 裁决生产库列型收敛方案(改 VARCHAR(32) 或统一走 event_uid 定位),本副本不再同时声称两种生成方。对外幂等与跨系统追溯一律使用 event_uidstr 64uk_event_uid不依赖 id 的物理类型。【复合主键偏离 · 已登记QC #4】summary[0].primary=[\"id\",\"created_at\"] 是对 spec「单键表恒为 [\\\"id\\\"]」的有意偏离:本表按 created_at 做按月 RANGE COLUMNS 分区MySQL/MariaDB 要求分区表所有唯一键(含主键)必须包含分区列,否则建表报错 1503。真源依据行号modules/pbl_runtime_ext/models/pbl_runtime_event.json:12primary=[\"id\",\"created_at\"])、同文件:6comment 中 1503 约束说明与分区保留 14 个月)、同文件:40fields[0].id type=long + auto_increment。本副本不写 partition/guards/engine 等私有键,但主键必须与真源一致,否则 uk_event_uid/uk_tenant_idem含 created_at无法在本副本声明的 PK 下成立。该偏离已在 M11b-2b-B 交付件正文登记DEVIATION(WS-1)),未写进校验器 COMPLIANCE_DEVIATIONS子任务 A 最终登记表为空 {},且本任务禁止修改校验器)。【私有键剥离】真源使用的 spec 未定义顶层扩展键 module/engine/charset/collate/tenant_scoped/append_only/write_paths/guards/partition/ddl_source以及 fields[].ddl_type/comment/auto_increment、indexes[].comment、codes[].comment本副本一律不保留:根键仅 summary/fields/indexes/codessummary[0] 仅 name/title/primary/catelog/commentfields[] 仅 name/title/type/length/dec/nullable/defaultindexes[] 仅 name/idxtype/idxfields(主键不写进 indexes由 summary[0].primary 表达)codes[] 仅 field/table/valuefield/textfield/cond。物理细节(分区、触发器、字符集、存储过程)以真源与其 DDL 派生源为准。【消费方清单】modules/world_sync/world_sync/pbl_runtime_sql.pyEVENT_TABLE/EVENT_COLUMNS由本文件 fields[] 机械导出、modules/world_sync/world_sync/pbl_runtime_tx.py::write_event_with_state单事务事件+状态写入、modules/world_sync/world_sync/pbl_runtime_authority.py列契约解析层已适配本内联四段式副本、modules/world_sync/world_sync/init.py事件写入门面。上述代码引用的列名必须全部出现在本文件 fields[] 中,核对证据见 M11b-2b-B 交付件「字段交叉核对清单」。【打包镜像】apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 是本文件的打包镜像,只由 modules/world_sync/scripts/sync_models_mirror.py 生成,禁止手工双写;一致性由 scripts/m11b2b_selftest.py 的 sha256 断言守护。"
}
],
"fields": [

View File

@ -1,25 +1,29 @@
# -*- coding: utf-8 -*-
"""pbl_runtime_authority —— M11b-2b``key=schema`` 引用的解析层(列名唯一权威源)。
"""pbl_runtime_authority —— M11b-2b表定义 JSON 的列契约解析层(列名唯一入口)。
**为什么存在**
PM 裁决任务 ``u0EO4x3ajntR41acaHj5H``方案 B``pbl_runtime_event`` /
``pbl_entity_state`` 两张表的**唯一真源** ``modules/pbl_runtime_ext/models/*.json``
M11b-1 已批准产物world_sync ``models/*.json`` 只保留 ``key=schema`` 引用
**不再自带** summary/fields/indexes/codes 结构体
**结构现状M11b-2b-B QC #1/#2/#3 整改)**
PM 裁决 B``pbl_runtime_event`` / ``pbl_entity_state`` **结构权威源**
``modules/pbl_runtime_ext/models/*.json``M11b-1 已批准产物禁止反向修改
QC 明确要求 world_sync ``models/*.json`` 必须是 **database-table-definition-spec
四段式summary/fields/indexes/codes内联副本**而不是 ``key=schema`` 引用式
校验器 ``scripts/validate_models_json.py`` 对引用式写法直接判 FAIL
因此本模块现在解析的是按真源逐字段内联并按 spec 归一抽象类型去私有键
world_sync 侧副本它同时就是 world_sync 消费方的列契约列名与真源一致
于是消费方列契约必须从权威定义**机械导出**而不是人工抄一遍人工双写正是
QC #1/#3 判定的「多份互相矛盾定义」的根因。本模块就是那个导出层:
本模块是列契约的**机械导出层**不是人工抄写层
* :func:`load_reference` 读本模块 ``models/<table>.json``key=schema 引用
* :func:`authority_model` 顺着引用解析到 pbl_runtime_ext 的真源 JSON
* :func:`load_model` 读本模块 ``models/<table>.json``四段式并校验结构
* :func:`columns` / :func:`insert_columns` / :func:`primary` / :func:`indexes`
暴露权威列集``insert_columns`` 自动剔除 auto_increment
* :func:`ddl` 由权威列集渲染本地建表语句仅供测试/本地起库生产 DDL
分区与 append-only 触发器归 pbl_runtime_ext ``scripts/pbl_runtime_event_ddl.py``
``scripts/migrations/*.sql`` 所有本模块不复制那套逻辑
暴露列契约``insert_columns`` 自动剔除自增列兼容真源私有 ``auto_increment``
* :func:`authority_path` 反查结构权威源pbl_runtime_ext在磁盘上的真实路径
供审计与 QC 取证写入结果的 ``authority`` 字段
* :func:`ddl` 由列契约渲染本地建表语句仅供测试/本地起库生产 DDL分区与
append-only 触发器归 pbl_runtime_ext ``scripts/pbl_runtime_event_ddl.py``
``scripts/migrations/*.sql`` 所有本模块不复制那套逻辑
* :func:`column_diff` 消费方用到的列 vs 列契约的双向 difffail-closed 的判据
**fail-closed**引用缺失真源找不到真源不是四段式列集为空 一律抛
:class:`AuthorityResolutionError`绝不退化成猜列名
**fail-closed**文件缺失JSON 不可解析不是四段式fields 为空 一律抛
:class:`AuthorityResolutionError`绝不退化成猜列名或回落到旧的手写常量
"""
import json
@ -27,10 +31,13 @@ import os
__all__ = [
"AuthorityResolutionError",
"REFERENCE_ROOT_KEYS",
"REFERENCE_SCHEMA_KEYS",
"SPEC_ROOT_KEYS",
"load_reference",
"SPEC_SUMMARY_KEYS",
"SPEC_FIELD_KEYS",
"SPEC_INDEX_KEYS",
"SPEC_CODE_KEYS",
"AUTHORITY_MODULE",
"load_model",
"authority_path",
"authority_model",
"columns",
@ -44,15 +51,16 @@ __all__ = [
"column_diff",
]
# models/<table>.json 是 key=schema 引用:根键只允许这两个
REFERENCE_ROOT_KEYS = ("key", "schema")
# 引用体内允许的元信息键(白名单,自测脚本按此做机械断言)
REFERENCE_SCHEMA_KEYS = (
"table", "authority_module", "authority_path", "owner", "milestone",
"decision_ref", "decision_basis", "structure_note", "consumers", "mirror_note",
)
# 真源(表定义)必须是四段式
# world_sync 侧 models/<table>.json 必须是 spec 四段式database-table-definition-spec
SPEC_ROOT_KEYS = ("summary", "fields", "indexes", "codes")
# 各段允许的键白名单自测脚本按此做机械断言comment 为 spec 允许的可选说明键)
SPEC_SUMMARY_KEYS = ("name", "title", "primary", "catelog", "comment")
SPEC_FIELD_KEYS = ("name", "title", "type", "length", "dec", "nullable", "default")
SPEC_INDEX_KEYS = ("name", "idxtype", "idxfields")
SPEC_CODE_KEYS = ("field", "table", "valuefield", "textfield", "cond")
# 结构权威源所在模块PM 裁决 B本模块只反查路径不消费其私有键
AUTHORITY_MODULE = "pbl_runtime_ext"
MODULE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MODELS_DIR = os.path.join(MODULE_ROOT, "models")
@ -77,7 +85,7 @@ _MYSQL_TYPES = {
class AuthorityResolutionError(Exception):
"""引用/真源解析失败fail-closed调用方不得吞掉"""
"""模型缺失/不可解析/非四段式fail-closed调用方不得吞掉"""
def __init__(self, message, **details):
Exception.__init__(self, message)
@ -91,61 +99,83 @@ class AuthorityResolutionError(Exception):
return "%s (%s)" % (self.message, parts)
def _reference_dir():
def _models_dir():
return MODELS_DIR
def load_reference(table):
"""读取本模块 ``models/<table>.json``,校验它是合法的 key=schema 引用。"""
if not table or not isinstance(table, str):
raise AuthorityResolutionError("table name is required", table=table)
path = os.path.join(_reference_dir(), table + ".json")
def _read_json(path):
if not os.path.isfile(path):
raise AuthorityResolutionError("schema reference missing", path=path)
raise AuthorityResolutionError("model file missing", path=path)
try:
with open(path, "r", encoding="utf-8") as fh:
ref = json.load(fh)
data = json.load(fh)
except ValueError as exc:
raise AuthorityResolutionError("schema reference not valid JSON: %s" % exc,
path=path)
if not isinstance(ref, dict) or ref.get("key") != "schema":
raise AuthorityResolutionError("model not valid JSON: %s" % exc, path=path)
if not isinstance(data, dict):
raise AuthorityResolutionError("model must be a JSON object", path=path)
return data
_MODEL_CACHE = {}
def load_model(table):
"""读取并校验本模块 ``models/<table>.json``spec 四段式内联副本)。
校验项fail-closed根键含 summary/fields/indexes/codessummary 恰好 1
fields 非空且每列有 name列集与真源的一致性由 scripts/m11b2b_column_diff.py
scripts/m11b2b_selftest.py 机械核对不在运行期重复解析真源私有键
"""
if not table or not isinstance(table, str):
raise AuthorityResolutionError("table name is required", table=table)
if table in _MODEL_CACHE:
return _MODEL_CACHE[table]
path = os.path.join(_models_dir(), table + ".json")
model = _read_json(path)
missing = [k for k in SPEC_ROOT_KEYS if k not in model]
if missing:
raise AuthorityResolutionError(
"models/%s.json must be a key=schema reference (no local structure)" % table,
path=path, root_keys=sorted(ref) if isinstance(ref, dict) else type(ref).__name__)
extra = set(ref) - set(REFERENCE_ROOT_KEYS)
if extra:
raise AuthorityResolutionError("reference carries non-whitelist root keys",
path=path, extra=sorted(extra))
schema = ref.get("schema")
if not isinstance(schema, dict):
raise AuthorityResolutionError("reference.schema must be an object", path=path)
if schema.get("table") != table:
raise AuthorityResolutionError("reference.schema.table mismatches filename",
path=path, declared=schema.get("table"))
bad = set(schema) - set(REFERENCE_SCHEMA_KEYS)
if bad:
raise AuthorityResolutionError("reference.schema carries non-whitelist keys",
path=path, extra=sorted(bad))
return ref
"model is not database-table-definition-spec four-section",
path=path, missing=missing,
hint="world_sync 侧必须是内联四段式副本PM 裁决 B + QC #1/#5不得用 key=schema 引用式")
summary = model.get("summary")
if not isinstance(summary, list) or len(summary) != 1 or not isinstance(summary[0], dict):
raise AuthorityResolutionError("summary must be an array with exactly 1 record",
path=path, got=type(summary).__name__)
if not str(summary[0].get("name") or "").strip():
raise AuthorityResolutionError("summary[0].name is required", path=path)
fields = model.get("fields")
if not isinstance(fields, list) or not fields:
raise AuthorityResolutionError("model has no fields", path=path)
for f in fields:
if not isinstance(f, dict) or not str(f.get("name") or "").strip():
raise AuthorityResolutionError("field without name", path=path, field=f)
_MODEL_CACHE[table] = model
return model
# 兼容旧调用名:本模块过去暴露 authority_model解析引用指向的真源现在列契约
# 就是内联副本本身,语义等价。
def authority_model(table):
"""列契约模型(= world_sync 侧内联四段式副本,逐字段抄自 pbl_runtime_ext 真源)。"""
return load_model(table)
def authority_path(table):
"""把引用解析成磁盘上真实存在的真源路径(多候选,全部落空即报错)。"""
ref = load_reference(table)
schema = ref["schema"]
mod = schema.get("authority_module")
candidates = []
declared = schema.get("authority_path")
if declared:
candidates.append(os.path.normpath(os.path.join(WORKSPACE_ROOT, declared)))
candidates.append(os.path.normpath(os.path.join(MODULE_ROOT, declared)))
if mod:
candidates.append(os.path.join(WORKSPACE_ROOT, "modules", mod, "models",
table + ".json"))
candidates.append(os.path.join(WORKSPACE_ROOT, "apps", "scense", "pkgs", mod,
"models", table + ".json"))
candidates.append(os.path.normpath(os.path.join(MODULE_ROOT, os.pardir, mod,
"models", table + ".json")))
"""反查结构权威源pbl_runtime_ext在磁盘上的真实路径供审计取证。
只用于写入结果里的 ``authority`` 字段与 QC 核对**不参与列集解析**列集来自
本模块内联副本找不到真源不抛错返回声明式相对路径真源缺失属部署期问题
不应让运行时写入失败
"""
declared = os.path.join("modules", AUTHORITY_MODULE, "models", table + ".json")
candidates = [
os.path.normpath(os.path.join(WORKSPACE_ROOT, declared)),
os.path.normpath(os.path.join(MODULE_ROOT, os.pardir, AUTHORITY_MODULE,
"models", table + ".json")),
os.path.join(WORKSPACE_ROOT, "apps", "scense", "pkgs", AUTHORITY_MODULE,
"models", table + ".json"),
]
tried = []
for cand in candidates:
if cand in tried:
@ -153,55 +183,28 @@ def authority_path(table):
tried.append(cand)
if os.path.isfile(cand):
return cand
raise AuthorityResolutionError("authority model not found for table",
table=table, tried=tried)
_MODEL_CACHE = {}
def authority_model(table):
"""读取并校验真源 JSON四段式 + fields 非空),带缓存。"""
if table in _MODEL_CACHE:
return _MODEL_CACHE[table]
path = authority_path(table)
try:
with open(path, "r", encoding="utf-8") as fh:
model = json.load(fh)
except ValueError as exc:
raise AuthorityResolutionError("authority model not valid JSON: %s" % exc,
path=path)
if not isinstance(model, dict):
raise AuthorityResolutionError("authority model must be an object", path=path)
missing = set(SPEC_ROOT_KEYS) - set(model)
if missing:
raise AuthorityResolutionError("authority model is not four-section",
path=path, missing=sorted(missing))
fields = model.get("fields")
if not isinstance(fields, list) or not fields:
raise AuthorityResolutionError("authority model has no fields", path=path)
_MODEL_CACHE[table] = model
return model
return declared
def field_map(table):
"""``{列名: 列定义 dict}``(保持真源顺序)。"""
"""``{列名: 列定义 dict}``(保持模型顺序)。"""
out = {}
for f in authority_model(table)["fields"]:
name = f.get("name")
if not name:
raise AuthorityResolutionError("authority field without name", table=table)
out[name] = f
for f in load_model(table)["fields"]:
out[f["name"]] = f
return out
def columns(table):
"""权威全列集(有序元组)"""
"""权威全列集(有序元组)—— 消费方 SQL 里允许出现的列名以此为唯一白名单。"""
return tuple(field_map(table).keys())
def auto_increment_columns(table):
"""由数据库自增生成的列INSERT 时不得出现)。"""
"""由数据库自增生成的列INSERT 时不得出现)。
内联副本按 spec 剥离了 ``auto_increment`` 私有键故通常为空集保留该逻辑是为了
当列契约指向真源 id auto_increment时仍能正确剔除该列
"""
return tuple(n for n, f in field_map(table).items()
if f.get("auto_increment") or str(f.get("default", "")).upper()
in ("AUTO_INCREMENT", "IDENTITY"))
@ -214,29 +217,38 @@ def insert_columns(table):
def primary(table):
summary = authority_model(table).get("summary") or []
if not summary or not isinstance(summary[0], dict):
raise AuthorityResolutionError("authority model has no summary[0]", table=table)
"""summary[0].primary非空数组。主键不写进 indexesspec 约定)。"""
summary = load_model(table).get("summary") or [{}]
pk = summary[0].get("primary") or summary[0].get("key")
if isinstance(pk, str):
pk = [p.strip() for p in pk.split(",") if p.strip()]
return tuple(pk or ())
if not isinstance(pk, list) or not pk:
raise AuthorityResolutionError("model summary[0].primary must be a non-empty array",
table=table)
cols = set(columns(table))
unknown = [p for p in pk if p not in cols]
if unknown:
raise AuthorityResolutionError("primary references unknown column",
table=table, unknown=unknown)
return tuple(pk)
def indexes(table):
"""规范化索引:``[{'name','idxtype','idxfields'}]``(兼容 unique/fields 写法)。"""
"""二级索引/唯一键列表:``[{name, idxtype, idxfields}]``(兼容真源 unique/fields 写法)。"""
out = []
for idx in authority_model(table).get("indexes") or []:
for idx in (load_model(table).get("indexes") or []):
if not isinstance(idx, dict):
raise AuthorityResolutionError("index must be an object", table=table, index=idx)
fields = idx.get("idxfields") or idx.get("fields") or []
if isinstance(fields, str):
fields = [f.strip() for f in fields.split(",") if f.strip()]
kind = idx.get("idxtype") or ("unique" if idx.get("unique") else "index")
out.append({"name": idx.get("name"), "idxtype": kind, "idxfields": tuple(fields)})
idxtype = idx.get("idxtype") or ("unique" if idx.get("unique") else "index")
out.append({"name": idx.get("name"), "idxtype": idxtype, "idxfields": list(fields)})
return out
def table_title(table):
summary = authority_model(table).get("summary") or [{}]
summary = load_model(table).get("summary") or [{}]
return summary[0].get("title") or summary[0].get("name") or table
@ -264,21 +276,19 @@ def _nullable(f):
def ddl(table, dialect="sqlite"):
"""权威列集渲染 CREATE TABLE测试/本地起库用)。
"""列契约渲染 CREATE TABLE测试/本地起库用)。
生产库的 DDL分区append-only 触发器存储过程 pbl_runtime_ext 所有
本函数**只保证列集与主键/唯一键一致**不复制分区与触发器逻辑
"""
if dialect not in ("sqlite", "mysql"):
raise AuthorityResolutionError("unsupported ddl dialect", dialect=dialect)
model = authority_model(table)
model = load_model(table)
gen = set(auto_increment_columns(table))
lines = []
for f in model["fields"]:
col = "`%s`" % f["name"] if dialect == "mysql" else f["name"]
col = ("`%s`" % f["name"]) if dialect == "mysql" else f["name"]
piece = [" %s %s" % (col, _render_type(f, dialect))]
gen = bool(f.get("auto_increment"))
if dialect == "mysql" and gen:
piece.append(" AUTO_INCREMENT")
if not _nullable(f) and not gen:
piece.append(" NOT NULL")
default = f.get("default")

View File

@ -2,8 +2,15 @@
"""M11b-2 运行时单事务写入 —— SQL 方言层(纯 SQL 文本 + 占位符,不绑定任何 ORM/驱动)。
职责边界本模块只负责 SQL 写对方言识别占位符风格READ COMMITTED 设置语句
悲观锁子句以及两张表的建表 DDL M11b-1 pbl_runtime_event schema 对齐
真正的事务编排在 :mod:`world_sync.pbl_runtime_tx`
悲观锁子句以及两张表的建表 DDL真正的事务编排在 :mod:`world_sync.pbl_runtime_tx`
**列集单一真源M11b-2b-B QC #3 整改)**
``EVENT_COLUMNS`` / ``STATE_COLUMNS`` 不再手写常量而是从
``modules/world_sync/models/pbl_runtime_event.json`` / ``pbl_entity_state.json``
spec 四段式内联副本结构权威源 = pbl_runtime_extPM 裁决 B ``fields[]``
**机械导出**旧版在本文件里手抄的 ``event_id`` / ``world_id``(state ) / ``state``
/ ``updated_by_event`` 等列名在真源中不存在构成代码一套JSON 另一套的双写分叉
QC #3 判定的消费方契约断裂根因;现在这类漂移在结构上不可能再发生。
设计要点
1. DB-API 2.0 规范接口``paramstyle`` / ``execute`` / ``rowcount`` / ``commit`` /
@ -12,11 +19,15 @@
``pyformat`` ``%(name)s``避免在 MySQL 上写出 sqlite ``?``
3. ``SELECT ... FOR UPDATE`` 只有 MySQL/PG 支持sqlite 侧返回空串sqlite 写锁由事务本身保证
并发正确性在两条路径上都由乐观锁 state_version兜底
4. 不引入任何广播 / 轮询 / 时延优化逻辑明确排除在 M11b-2 范围之外
4. 不引入任何广播 / 轮询 / 时延优化逻辑明确排除在 M11b-2 范围之外
5. 建表 DDL :func:`world_sync.pbl_runtime_authority.ddl` 按同一份模型渲染
本模块不再自带第二套列定义生产库的分区/触发器 DDL pbl_runtime_ext 所有
"""
import sqlite3
from . import pbl_runtime_authority as auth
__all__ = [
"EVENT_TABLE",
"STATE_TABLE",
@ -34,6 +45,7 @@ __all__ = [
"ddl_for",
"EVENT_COLUMNS",
"STATE_COLUMNS",
"columns_of",
"percentile",
]
@ -44,30 +56,24 @@ DIALECT_SQLITE = "sqlite"
DIALECT_MYSQL = "mysql"
DIALECT_POSTGRES = "postgres"
# 与 M11b-1 dev-notes-m11b1-pbl_runtime_event.md 的表定义对齐append-only无 updated_at
EVENT_COLUMNS = (
"event_id", # 事件主键uuid/ULID由调用方或本模块生成
"tenant_id", # 租户,强制打头,缺失即拒绝写入
"world_id", # 世界隔离维度,强制
"session_id", # 运行时会话(可空)
"entity_id", # 关联实体
"event_type", # 事件类型(如 state.updated
"payload", # 事件体JSON 文本)
"causation_id", # 因果链上游事件(可空)
"source", # 写入来源标识(模块名/引擎版本)
"state_version", # 本事件落库后实体的状态版本(与 pbl_entity_state 一致,便于回放)
"created_at", # 服务端时间戳TEXT ISO8601 UTC
)
STATE_COLUMNS = (
"tenant_id",
"world_id",
"entity_id",
"state", # 实体状态JSON 文本)
"state_version", # 乐观锁版本号,每次成功写入 +1
"updated_at",
"updated_by_event",
)
def columns_of(table):
"""取某表在模型 JSONspec 四段式内联副本)里声明的全列集(有序)。
fail-closed模型缺失 / 不是四段式 / fields 为空一律由
:mod:`world_sync.pbl_runtime_authority`
:class:`~world_sync.pbl_runtime_authority.AuthorityResolutionError`不退回旧常量
"""
return auth.columns(table)
# append-only 事件流列集(= models/pbl_runtime_event.json fields[] 顺序25 列,含应用层
# 生成的 idid 生成方见 world_sync/pbl_runtime_tx.py::new_event_uid 与该文件 summary[0].comment
EVENT_COLUMNS = columns_of(EVENT_TABLE)
# 实体状态列集(= models/pbl_entity_state.json fields[] 顺序10 列;定位键是
# tenant_id + session_id + entity_id对应唯一键 uk_es真源无 world_id / state 列)
STATE_COLUMNS = columns_of(STATE_TABLE)
def detect_dialect(conn_or_module):
@ -162,80 +168,18 @@ def supports_transaction_control(dialect):
def _ddl_sqlite():
"""返回 sqlite 建表语句列表(单测 / 本地环境用)。"""
return [
"""
CREATE TABLE IF NOT EXISTS %s (
event_id TEXT PRIMARY KEY,
tenant_id TEXT NOT NULL,
world_id TEXT NOT NULL,
session_id TEXT,
entity_id TEXT NOT NULL,
event_type TEXT NOT NULL,
payload TEXT NOT NULL,
causation_id TEXT,
source TEXT,
state_version INTEGER NOT NULL,
created_at TEXT NOT NULL
)
""" % EVENT_TABLE,
"""
CREATE INDEX IF NOT EXISTS idx_pbl_runtime_event_world_time
ON %s (tenant_id, world_id, created_at)
""" % EVENT_TABLE,
"""
CREATE INDEX IF NOT EXISTS idx_pbl_runtime_event_entity
ON %s (tenant_id, world_id, entity_id, state_version)
""" % EVENT_TABLE,
"""
CREATE TABLE IF NOT EXISTS %s (
tenant_id TEXT NOT NULL,
world_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
state TEXT NOT NULL,
state_version INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
updated_by_event TEXT,
PRIMARY KEY (tenant_id, world_id, entity_id)
)
""" % STATE_TABLE,
]
"""sqlite 建表语句列表(单测 / 本地环境用),由模型 JSON 渲染,无第二套列定义。"""
return [auth.ddl(EVENT_TABLE, DIALECT_SQLITE), auth.ddl(STATE_TABLE, DIALECT_SQLITE)]
def _ddl_mysql():
"""返回 MySQL 建表语句列表生产环境用InnoDB + utf8mb4"""
return [
"""
CREATE TABLE IF NOT EXISTS `%s` (
`event_id` VARCHAR(64) NOT NULL COMMENT '事件主键',
`tenant_id` VARCHAR(64) NOT NULL COMMENT '租户ID(强制)',
`world_id` VARCHAR(64) NOT NULL COMMENT '世界ID(隔离维度)',
`session_id` VARCHAR(64) DEFAULT NULL COMMENT '运行时会话ID',
`entity_id` VARCHAR(64) NOT NULL COMMENT '关联实体ID',
`event_type` VARCHAR(64) NOT NULL COMMENT '事件类型',
`payload` JSON NOT NULL COMMENT '事件体',
`causation_id` VARCHAR(64) DEFAULT NULL COMMENT '因果链上游事件',
`source` VARCHAR(64) DEFAULT NULL COMMENT '写入来源',
`state_version` BIGINT NOT NULL COMMENT '落库后实体状态版本',
`created_at` DATETIME(3) NOT NULL COMMENT '服务端时间戳',
PRIMARY KEY (`event_id`),
KEY `idx_pbl_runtime_event_world_time` (`tenant_id`, `world_id`, `created_at`),
KEY `idx_pbl_runtime_event_entity` (`tenant_id`, `world_id`, `entity_id`, `state_version`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL运行时事件(append-only,M11b-1)'
""" % EVENT_TABLE,
"""
CREATE TABLE IF NOT EXISTS `%s` (
`tenant_id` VARCHAR(64) NOT NULL COMMENT '租户ID(强制)',
`world_id` VARCHAR(64) NOT NULL COMMENT '世界ID(隔离维度)',
`entity_id` VARCHAR(64) NOT NULL COMMENT '实体ID',
`state` JSON NOT NULL COMMENT '实体状态',
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
`updated_at` DATETIME(3) NOT NULL COMMENT '更新时间',
`updated_by_event` VARCHAR(64) DEFAULT NULL COMMENT '最近生效事件',
PRIMARY KEY (`tenant_id`, `world_id`, `entity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='PBL实体状态(单事务写入,M11b-2)'
""" % STATE_TABLE,
]
"""MySQL 建表语句列表(本地/预发用InnoDB + utf8mb4同样由模型 JSON 渲染。
生产库真正的 ``pbl_runtime_event`` DDL按月 RANGE COLUMNS 分区append-only 存储过程
与触发器 pbl_runtime_ext ``scripts/pbl_runtime_event_ddl.py``
``scripts/migrations/*.sql`` 所有本函数不复制那套物理细节
"""
return [auth.ddl(EVENT_TABLE, DIALECT_MYSQL), auth.ddl(STATE_TABLE, DIALECT_MYSQL)]
#: 各方言建表语句集合,供宿主初始化 / 单测建库使用。