deliver: 交付收口(引擎代为提交)
This commit is contained in:
parent
aca3e3117a
commit
882a1b5cd3
106
__init__.py
106
__init__.py
@ -1,9 +1,105 @@
|
||||
from .world_sync.init import (
|
||||
load_world_sync, list_world_syncs, get_world_sync, create_world_sync,
|
||||
update_world_sync, delete_world_sync, execute_world_sync,
|
||||
"""world_sync 模块根包(对外统一导出面)。
|
||||
|
||||
三层接线(M11b-2):
|
||||
- ``world_sync.world_sync``:内层实现包 —— 单事务「事件 + 实体状态」原子写入契约
|
||||
(纯标准库依赖,异常族与读写入口在此 re-export);
|
||||
- ``world_sync.world_sync.init``:宿主挂载层 —— world_sync CRUD、M11b-1 表注册、
|
||||
M11b-2 conn_factory 登记到 ServerEnv;
|
||||
- 本文件:把两层能力合并成模块级 API,loader 函数名统一为 ``load_world_sync``。
|
||||
|
||||
铁律:本包不硬编码库名(取库名走 ``ServerEnv().get_module_dbname('world_sync')``),
|
||||
不在导入期建立任何数据库连接(连接工厂缺失时由写入调用点 fail-closed 抛异常)。
|
||||
"""
|
||||
|
||||
# --- M11b-2: 单事务写入契约(最先导出,保证 import 闭包零断裂) ---
|
||||
from .world_sync import (
|
||||
MODULE_NAME,
|
||||
MODULE_VERSION,
|
||||
SERVER_ENV_CONN_FACTORY_KEYS,
|
||||
ConcurrentStateConflict,
|
||||
ConnFactoryNotConfigured,
|
||||
EventWriteError,
|
||||
PblRuntimeError,
|
||||
StateWriteError,
|
||||
TxAbortError,
|
||||
TxConfigError,
|
||||
build_sqlite_conn_factory,
|
||||
detect_dialect,
|
||||
new_event_id,
|
||||
percentile,
|
||||
read_entity_state,
|
||||
resolve_conn_factory,
|
||||
write_event_with_state,
|
||||
)
|
||||
from .world_sync import load_world_sync as _mount_runtime
|
||||
|
||||
# --- 既有 CRUD / 表注册能力(依赖宿主基础模块,缺失时降级不阻断契约导入) ---
|
||||
try:
|
||||
from .world_sync.init import (
|
||||
load_world_sync as _mount_crud,
|
||||
list_world_syncs,
|
||||
get_world_sync,
|
||||
create_world_sync,
|
||||
update_world_sync,
|
||||
delete_world_sync,
|
||||
execute_world_sync,
|
||||
module_tables,
|
||||
get_table_schema,
|
||||
)
|
||||
except Exception as _exc: # pragma: no cover - 仅在宿主基础模块缺失时触发
|
||||
_mount_crud = None
|
||||
_WORLD_SYNC_INIT_ERROR = _exc
|
||||
list_world_syncs = get_world_sync = create_world_sync = None
|
||||
update_world_sync = delete_world_sync = execute_world_sync = None
|
||||
module_tables = get_table_schema = None
|
||||
else:
|
||||
_WORLD_SYNC_INIT_ERROR = None
|
||||
|
||||
__all__ = [
|
||||
"load_world_sync", "list_world_syncs", "get_world_sync", "create_world_sync",
|
||||
"update_world_sync", "delete_world_sync", "execute_world_sync",
|
||||
# 挂载入口(三处接线统一名)
|
||||
"load_world_sync",
|
||||
# M11b-2 写入 / 读取契约
|
||||
"write_event_with_state",
|
||||
"read_entity_state",
|
||||
"new_event_id",
|
||||
"percentile",
|
||||
"detect_dialect",
|
||||
"resolve_conn_factory",
|
||||
"build_sqlite_conn_factory",
|
||||
# 接线常量
|
||||
"MODULE_NAME",
|
||||
"MODULE_VERSION",
|
||||
"SERVER_ENV_CONN_FACTORY_KEYS",
|
||||
# 异常族(fail-closed)
|
||||
"PblRuntimeError",
|
||||
"TxConfigError",
|
||||
"ConnFactoryNotConfigured",
|
||||
"EventWriteError",
|
||||
"StateWriteError",
|
||||
"ConcurrentStateConflict",
|
||||
"TxAbortError",
|
||||
# 既有 CRUD 能力(宿主基础模块可用时为真函数)
|
||||
"list_world_syncs",
|
||||
"get_world_sync",
|
||||
"create_world_sync",
|
||||
"update_world_sync",
|
||||
"delete_world_sync",
|
||||
"execute_world_sync",
|
||||
"module_tables",
|
||||
"get_table_schema",
|
||||
]
|
||||
|
||||
|
||||
def load_world_sync(server_env=None, conn_factory=None):
|
||||
"""统一挂载入口:既有 CRUD/表注册 + M11b-2 运行时写入契约登记。
|
||||
|
||||
:param server_env: 宿主 ServerEnv 实例(None 时由内层实现自行取全局单例)
|
||||
:param conn_factory: 宿主连接工厂;提供则登记到 ``pbl_runtime_conn_factory``,
|
||||
使写入契约无需知道任何部署环境凭据来源
|
||||
:return: 挂载后的 ServerEnv(宿主基础模块不可用时返回传入的 env)
|
||||
"""
|
||||
env = server_env
|
||||
if _mount_crud is not None:
|
||||
env = _mount_crud() or env
|
||||
_mount_runtime(server_env=env, conn_factory=conn_factory)
|
||||
return env
|
||||
|
||||
@ -3,28 +3,88 @@
|
||||
{
|
||||
"name": "pbl_entity_state",
|
||||
"title": "实体当前状态(乐观锁版本表)",
|
||||
"primary": ["id"],
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
"catelog": "relation",
|
||||
"comment": "PBL 实体当前状态表(world_sync 侧,M11b-2 新增)。保存世界内实体的最新完整状态快照,state_version 为乐观锁版本号(每次成功写入 +1),用于防止并发覆盖(丢失更新)。【主键与唯一性】规范主键 id 为 str(32)(应用层生成 uuid4().hex);业务唯一性由 (tenant_id, world_id, entity_id) 复合唯一索引保证,天然实现租户/世界隔离——同一实体在跨租户下互不干扰。【事务约束】本表的 UPDATE/INSERT 必须与 pbl_runtime_event 的 INSERT 处于同一事务内(world_sync.pbl_runtime_tx.write_event_with_state),任一失败整事务回滚;禁止脱离事务单独更新本表。【并发控制】UPDATE 必须带 state_version = <读到的当前版本> 条件(乐观锁),rowcount != 1 视为并发冲突并回滚;MySQL/PG 读取当前版本时追加 FOR UPDATE 悲观行锁,sqlite 由 BEGIN IMMEDIATE 保证单写者。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 STATE_COLUMNS 及 pbl_runtime_tx.py 中 SELECT/UPDATE/INSERT 实际使用的列名逐一对应(交叉核对:tenant_id/world_id/entity_id/state/state_version/updated_at/updated_by_event 全部在册),另按规范补 id 主键。【与 pbl_runtime_ext 的差异】modules/pbl_runtime_ext/models/pbl_entity_state.json(M11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id,属另一张同名表的另一套定义,两者不可混用;本文件为 world_sync(M11b-2)写入路径的权威定义,双真源合并方案已登记待 PM 裁决。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生(str→VARCHAR、int→INT、text→TEXT、datetime→DATETIME),本文件不出现 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型。",
|
||||
"module": "world_sync",
|
||||
"owner_module": "world_sync",
|
||||
"milestone": "M11b-2",
|
||||
"tenant_scoped": true
|
||||
"comment": "PBL 实体当前状态表(world_sync 侧,M11b-2 新增)。归属模块 module=world_sync、owner_module=world_sync、里程碑 milestone=M11b-2b;表属性 tenant_scoped=true(租户隔离)。保存世界内实体的最新完整状态快照,state_version 为乐观锁版本号(每次成功写入 +1),用于防止并发覆盖(丢失更新)。【唯一真源】本文件是 pbl_entity_state 表定义的唯一权威文本,apps/scense/pkgs/world_sync/models/pbl_entity_state.json 只是由 scripts/sync_models_to_app.py(或 cp)从本文件同步出的打包镜像,禁止手工双写,二者 sha256 必须相同(由 scripts/m11b2_selftest.py 机械断言)。【主键与唯一性】规范主键 id 为 str(32)(应用层生成 uuid4().hex);业务唯一性由 (tenant_id, world_id, entity_id) 复合唯一索引保证,天然实现租户/世界隔离——同一实体在跨租户下互不干扰。【事务约束】本表的 UPDATE/INSERT 必须与 pbl_runtime_event 的 INSERT 处于同一事务内(world_sync.pbl_runtime_tx.write_event_with_state),任一失败整事务回滚;禁止脱离事务单独更新本表。【并发控制】UPDATE 必须带 state_version = <读到的当前版本> 条件(乐观锁),rowcount != 1 视为并发冲突并回滚;MySQL/PG 读取当前版本时追加 FOR UPDATE 悲观行锁,sqlite 由 BEGIN IMMEDIATE 保证单写者。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 STATE_COLUMNS 及 pbl_runtime_tx.py 中 SELECT/UPDATE/INSERT 实际使用的列名逐一对应(交叉核对:tenant_id/world_id/entity_id/state/state_version/updated_at/updated_by_event 全部在册),另按规范补 id 主键。【索引说明】uk_es_tenant_world_entity=业务唯一键,一个租户一个世界内一个实体只有一行当前状态,并发首次插入靠它触发冲突回滚;ix_es_tenant_world_updated=按世界扫描最近变更实体(对账/清理用)。【与 pbl_runtime_ext 的差异】modules/pbl_runtime_ext/models/pbl_entity_state.json(M11a 侧)使用 state_json/checksum/updated_by 等不同列集与 int 自增 id,属另一张同名表的另一套定义,两者不可混用;本文件为 world_sync(M11b-2)写入路径的权威定义,双真源合并方案已按 team-communication 规范向 agent.pm 冒泡请求裁决(见交付摘要冒泡单据)。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生(str→VARCHAR、int→INT、text→TEXT、datetime→DATETIME),本文件不出现 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型。【键白名单】summary 仅含 name/title/primary/catelog/comment;fields 仅含 name/title/type/length/dec/nullable/default(说明文字一律写进 title,DDL 模板按 title 渲染 COMMENT);indexes 仅含 name/idxtype/idxfields;根键仅 summary/fields/indexes/codes 四段式。"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no", "comment": "规范主键(str(32)),应用层生成 uuid4().hex;首次 INSERT 状态行时创建"},
|
||||
{"name": "tenant_id", "title": "租户ID", "type": "str", "length": 64, "nullable": "no", "comment": "租户隔离维度,所有读写 WHERE 打头列"},
|
||||
{"name": "world_id", "title": "世界ID", "type": "str", "length": 64, "nullable": "no", "comment": "世界隔离维度,与 tenant_id 共同限定作用域"},
|
||||
{"name": "entity_id", "title": "实体ID", "type": "str", "length": 64, "nullable": "no", "comment": "实体标识,(tenant_id, world_id, entity_id) 定位唯一状态行"},
|
||||
{"name": "state", "title": "实体状态", "type": "text", "nullable": "no", "comment": "实体当前完整状态,JSON 文本(规范抽象类型 text,不用方言 JSON 类型;序列化见 pbl_runtime_tx._dumps)"},
|
||||
{"name": "state_version", "title": "状态版本号", "type": "int", "nullable": "no", "default": "0", "comment": "乐观锁版本号,每次成功写入 +1;与 pbl_runtime_event.state_version 保持一致"},
|
||||
{"name": "updated_at", "title": "最近生效时间", "type": "datetime", "nullable": "no", "comment": "最近一次生效的 UTC 时间戳(应用层写入 ISO8601 文本,见 pbl_runtime_tx.utc_now_text)"},
|
||||
{"name": "updated_by_event", "title": "最近生效事件ID", "type": "str", "length": 64, "nullable": "yes", "comment": "最近生效的事件业务键,回指 pbl_runtime_event.event_id"}
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID(str32,应用层生成uuid4().hex,首次INSERT状态行时创建)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"title": "租户ID(隔离维度,所有读写WHERE打头列)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "world_id",
|
||||
"title": "世界ID(隔离维度,与tenant_id共同限定作用域)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "entity_id",
|
||||
"title": "实体ID(tenant_id+world_id+entity_id定位唯一状态行)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "state",
|
||||
"title": "实体当前完整状态(JSON文本,规范抽象类型text,不用方言JSON类型)",
|
||||
"type": "text",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "state_version",
|
||||
"title": "状态版本号(乐观锁,每次成功写入+1,与pbl_runtime_event.state_version一致)",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "最近生效时间(UTC时间戳,应用层写入ISO8601文本)",
|
||||
"type": "datetime",
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "updated_by_event",
|
||||
"title": "最近生效事件ID(回指pbl_runtime_event.event_id业务键)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "yes"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "uk_es_tenant_world_entity", "idxtype": "unique", "idxfields": ["tenant_id", "world_id", "entity_id"], "comment": "业务唯一键:一个租户一个世界内一个实体只有一行当前状态;并发首次插入靠它触发冲突回滚"},
|
||||
{"name": "ix_es_tenant_world_updated", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "updated_at"], "comment": "按世界扫描最近变更实体(对账/清理用)"}
|
||||
{
|
||||
"name": "uk_es_tenant_world_entity",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"world_id",
|
||||
"entity_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ix_es_tenant_world_updated",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"world_id",
|
||||
"updated_at"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
|
||||
@ -3,38 +3,152 @@
|
||||
{
|
||||
"name": "pbl_runtime_event",
|
||||
"title": "运行时事件流(append-only)",
|
||||
"primary": ["id"],
|
||||
"primary": [
|
||||
"id"
|
||||
],
|
||||
"catelog": "relation",
|
||||
"comment": "PBL 运行时事件表(world_sync 侧写入视图,M11b-2 在单事务内写入的目标表)。【写入约束】append-only:只允许 INSERT,禁止 UPDATE/DELETE;每条事件代表一次运行时状态变更,供回放与审计。【事务约束】本表的 INSERT 必须与 pbl_entity_state 的更新处于同一数据库事务内(见 world_sync.pbl_runtime_tx.write_event_with_state),任一失败整事务回滚,不允许脱离事务单独写事件。【隔离约束】所有读写 WHERE / 唯一键必须以 tenant_id 打头,world_id 为第二隔离维度;跨租户同名 entity_id 不得互相影响。【ID 约定】主键 id 为 str(32),由应用层生成(uuid4().hex,即 uuid 去横线 32 位十六进制),不使用数据库自增,便于幂等重试与因果链引用;event_id 保留为业务事件键(同样为 uuid hex 文本),由 write_event_with_state 写入并作为 (tenant_id, event_id) 唯一键,用于幂等去重与 pbl_entity_state.updated_by_event 回指。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 EVENT_COLUMNS 及 pbl_runtime_tx.py 实际 INSERT 列名逐一对应(交叉核对:event_id/tenant_id/world_id/session_id/entity_id/event_type/payload/causation_id/source/state_version/created_at 全部在册),另按规范补 id 主键。【与 M11b-1 的关系】modules/pbl_runtime_ext/models/pbl_runtime_event.json 是 M11b-1(pbl_runtime_ext)侧的同一表名定义,其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS(25 列、复合主键 (id, created_at)、id 为 long 自增、按 created_at 做按月 RANGE COLUMNS 分区、保留 14 个月)。两者同名不同构,属双真源风险,已登记待 PM 裁决合并方案(本任务只规范 world_sync 侧定义,不改动 M11b-1 已批准的 json 与其 DDL 生成器)。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生(str→VARCHAR、int→INT、text→TEXT/LONGTEXT、datetime→DATETIME),本文件不出现任何数据库方言具体类型。",
|
||||
"module": "world_sync",
|
||||
"owner_module": "world_sync",
|
||||
"milestone": "M11b-2",
|
||||
"append_only": true,
|
||||
"tenant_scoped": true
|
||||
"comment": "PBL 运行时事件表(world_sync 侧写入视图)。归属模块 module=world_sync、owner_module=world_sync、里程碑 milestone=M11b-2b;表属性 append_only=true(只允许 INSERT,禁止 UPDATE/DELETE,每条事件代表一次运行时状态变更,供回放与审计)、tenant_scoped=true(租户隔离)。【唯一真源】本文件是 pbl_runtime_event 表定义的唯一权威文本,apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 只是由 scripts/sync_models_to_app.py(或 cp)从本文件同步出的打包镜像,禁止手工双写,二者 sha256 必须相同(由 scripts/m11b2_selftest.py 机械断言)。【写入约束】append-only:只允许 INSERT,禁止 UPDATE/DELETE。【事务约束】本表的 INSERT 必须与 pbl_entity_state 的更新处于同一数据库事务内(见 world_sync.pbl_runtime_tx.write_event_with_state),任一失败整事务回滚,不允许脱离事务单独写事件。【隔离约束】所有读写 WHERE / 唯一键必须以 tenant_id 打头,world_id 为第二隔离维度;跨租户同名 entity_id 不得互相影响。【ID 约定】主键 id 为 str(32),由应用层生成(uuid4().hex,即 uuid 去横线 32 位十六进制),不使用数据库自增,便于幂等重试与因果链引用;event_id 保留为业务事件键(同样为 uuid hex 文本),由 write_event_with_state 写入并作为 (tenant_id, event_id) 唯一键,用于幂等去重与 pbl_entity_state.updated_by_event 回指。【列真源】fields 与 world_sync/pbl_runtime_sql.py 的 EVENT_COLUMNS 及 pbl_runtime_tx.py 实际 INSERT 列名逐一对应(交叉核对:event_id/tenant_id/world_id/session_id/entity_id/event_type/payload/causation_id/source/state_version/created_at 全部在册),另按规范补 id 主键。【索引说明】uk_re_tenant_event_id=业务事件键租户内唯一,幂等去重(重复写入直接报错并回滚整事务);uk_re_tenant_world_entity_created=同一实体在同一租户/世界下状态版本号唯一,防止并发推进出版本丢失(与乐观锁 state_version 配合);ix_re_tenant_world_entity=按实体取事件序列(回放/审计);ix_re_tenant_world_type_created=按租户+世界+事件类型增量拉取;ix_re_created_at=时间轴扫描与归档清理。【与 M11b-1 的关系】modules/pbl_runtime_ext/models/pbl_runtime_event.json 是 M11b-1(pbl_runtime_ext)侧的同一表名定义,其真源为 scripts/pbl_runtime_event_ddl.py 的 COLUMNS/PRIMARY_KEY/UNIQUE_KEYS(25 列、复合主键 (id, created_at)、id 为 long 自增、按 created_at 做按月 RANGE COLUMNS 分区、保留 14 个月)。两者同名不同构,属双真源风险,已按 team-communication 规范向 agent.pm 冒泡请求裁决(见交付摘要冒泡单据),本任务不擅自改动 M11b-1 已批准的 json 与其 DDL 生成器。【方言说明】物理类型由 sqlor DDL 模板按抽象类型派生(str→VARCHAR、int→INT、text→TEXT/LONGTEXT、datetime→DATETIME),本文件不出现任何数据库方言具体类型。【键白名单】summary 仅含 name/title/primary/catelog/comment;fields 仅含 name/title/type/length/dec/nullable/default(说明文字一律写进 title,DDL 模板按 title 渲染 COMMENT);indexes 仅含 name/idxtype/idxfields;根键仅 summary/fields/indexes/codes 四段式。"
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{"name": "id", "title": "主键ID", "type": "str", "length": 32, "nullable": "no", "comment": "规范主键(str(32)),应用层生成 uuid4().hex;非数据库自增"},
|
||||
{"name": "event_id", "title": "业务事件ID", "type": "str", "length": 64, "nullable": "no", "comment": "业务事件键(uuid hex 文本),幂等去重用;被 pbl_entity_state.updated_by_event 回指"},
|
||||
{"name": "tenant_id", "title": "租户ID", "type": "str", "length": 64, "nullable": "no", "comment": "租户隔离维度,强制打头,缺失即拒绝写入"},
|
||||
{"name": "world_id", "title": "世界ID", "type": "str", "length": 64, "nullable": "no", "comment": "世界隔离维度,强制"},
|
||||
{"name": "session_id", "title": "运行时会话ID", "type": "str", "length": 64, "nullable": "yes", "comment": "运行时会话(可空,无会话上下文时为空)"},
|
||||
{"name": "entity_id", "title": "实体ID", "type": "str", "length": 64, "nullable": "no", "comment": "事件关联的实体"},
|
||||
{"name": "event_type", "title": "事件类型", "type": "str", "length": 64, "nullable": "no", "default": "state.updated", "comment": "事件类型编码,如 state.updated(默认值与 pbl_runtime_tx.DEFAULT_EVENT_TYPE 一致)"},
|
||||
{"name": "payload", "title": "事件体", "type": "text", "nullable": "yes", "comment": "事件负载,JSON 文本(规范抽象类型 text,不用方言 JSON 类型;序列化见 pbl_runtime_tx._dumps)"},
|
||||
{"name": "causation_id", "title": "因果上游事件ID", "type": "str", "length": 64, "nullable": "yes", "comment": "因果链上游事件(可空)"},
|
||||
{"name": "source", "title": "写入来源", "type": "str", "length": 64, "nullable": "no", "default": "world_sync.m11b2", "comment": "写入来源标识(模块名/引擎版本,默认值与 pbl_runtime_tx.DEFAULT_SOURCE 一致)"},
|
||||
{"name": "state_version", "title": "状态版本", "type": "int", "nullable": "no", "default": "0", "comment": "本事件落库后实体的状态版本,与 pbl_entity_state.state_version 一致,便于回放对账"},
|
||||
{"name": "created_at", "title": "创建时间", "type": "datetime", "nullable": "no", "comment": "服务端时间戳(应用层写入 ISO8601 UTC 文本,见 pbl_runtime_tx.utc_now_text);append-only 表不设 updated_at"}
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID(str32,应用层生成uuid4().hex,非数据库自增)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "event_id",
|
||||
"title": "业务事件ID(uuid hex文本,幂等去重键,被pbl_entity_state.updated_by_event回指)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "tenant_id",
|
||||
"title": "租户ID(隔离维度,强制打头,缺失即拒绝写入)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "world_id",
|
||||
"title": "世界ID(隔离维度,强制)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "session_id",
|
||||
"title": "运行时会话ID(可空,无会话上下文时为空)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "yes"
|
||||
},
|
||||
{
|
||||
"name": "entity_id",
|
||||
"title": "实体ID(事件关联的实体)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "event_type",
|
||||
"title": "事件类型(如state.updated,默认值同pbl_runtime_tx.DEFAULT_EVENT_TYPE)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no",
|
||||
"default": "state.updated"
|
||||
},
|
||||
{
|
||||
"name": "payload",
|
||||
"title": "事件体(JSON文本,规范抽象类型text,不用方言JSON类型)",
|
||||
"type": "text",
|
||||
"nullable": "yes"
|
||||
},
|
||||
{
|
||||
"name": "causation_id",
|
||||
"title": "因果上游事件ID(可空)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "yes"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "写入来源(模块名/引擎版本,默认值同pbl_runtime_tx.DEFAULT_SOURCE)",
|
||||
"type": "str",
|
||||
"length": 64,
|
||||
"nullable": "no",
|
||||
"default": "world_sync.m11b2"
|
||||
},
|
||||
{
|
||||
"name": "state_version",
|
||||
"title": "状态版本(本事件落库后实体状态版本,与pbl_entity_state.state_version一致,便于回放对账)",
|
||||
"type": "int",
|
||||
"nullable": "no",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间(服务端UTC时间戳,应用层写入ISO8601文本;append-only表不设updated_at)",
|
||||
"type": "datetime",
|
||||
"nullable": "no"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{"name": "uk_re_tenant_event_id", "idxtype": "unique", "idxfields": ["tenant_id", "event_id"], "comment": "业务事件键租户内唯一,幂等去重(重复写入直接报错并回滚整事务)"},
|
||||
{"name": "uk_re_tenant_world_entity_created", "idxtype": "unique", "idxfields": ["tenant_id", "world_id", "entity_id", "state_version"], "comment": "同一实体在同一租户/世界下版本号唯一,防止并发推进出版本丢失(与乐观锁 state_version 配合)"},
|
||||
{"name": "ix_re_tenant_world_entity", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "entity_id"], "comment": "按实体取事件序列(回放/审计)"},
|
||||
{"name": "ix_re_tenant_world_type_created", "idxtype": "index", "idxfields": ["tenant_id", "world_id", "event_type", "created_at"], "comment": "按租户+世界+事件类型增量拉取"},
|
||||
{"name": "ix_re_created_at", "idxtype": "index", "idxfields": ["created_at"], "comment": "时间轴扫描与归档清理"}
|
||||
{
|
||||
"name": "uk_re_tenant_event_id",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"event_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "uk_re_tenant_world_entity_created",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"world_id",
|
||||
"entity_id",
|
||||
"state_version"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ix_re_tenant_world_entity",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"world_id",
|
||||
"entity_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ix_re_tenant_world_type_created",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"tenant_id",
|
||||
"world_id",
|
||||
"event_type",
|
||||
"created_at"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ix_re_created_at",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"created_at"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{"field": "event_type", "table": "appcodes_kv", "valuefield": "k", "textfield": "v", "cond": "parentid='pbl_event_type'"}
|
||||
{
|
||||
"field": "event_type",
|
||||
"table": "appcodes_kv",
|
||||
"valuefield": "k",
|
||||
"textfield": "v",
|
||||
"cond": "parentid='pbl_event_type'"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,217 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M11b-2 自测脚本:真实跑通「单事务事件+状态写入」的原子性 / 回滚 / 并发 / P99 时延。
|
||||
"""M11b-2b 表定义自检(database-table-definition-spec 机械断言)。
|
||||
|
||||
与 tests/test_m11b2_wiring.py 同口径(同一 percentile 实现、同一 sqlite 方言),
|
||||
输出可直接粘贴进 dev-notes 的实测证据。运行:
|
||||
覆盖范围(QC #4 要求):
|
||||
1. 全部 4 个落点都跑同一套 spec 断言:
|
||||
- modules/world_sync/models/pbl_runtime_event.json (唯一真源)
|
||||
- modules/world_sync/models/pbl_entity_state.json (唯一真源)
|
||||
- apps/scense/pkgs/world_sync/models/pbl_runtime_event.json (打包镜像)
|
||||
- apps/scense/pkgs/world_sync/models/pbl_entity_state.json (打包镜像)
|
||||
2. 真源与镜像 sha256 两两相同的一致性断言(消除内容级分叉)。
|
||||
3. 各段键集合 ⊆ spec 白名单的断言(summary/fields/indexes/codes 均不得出现未定义键)。
|
||||
4. 抽象类型断言(禁止 VARCHAR/BIGINT/DATETIME(3)/JSON 等方言具体类型)。
|
||||
5. 主键 id str(32) 断言 + 与 pbl_runtime_sql.py 实际列名交叉核对。
|
||||
|
||||
cd apps/scense/pkgs/world_sync
|
||||
../../venv/bin/python scripts/m11b2_selftest.py
|
||||
任一断言不符即 RESULT: FAIL 且退出码 1。
|
||||
用法: python3 modules/world_sync/scripts/m11b2_selftest.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
MODULE_DIR = os.path.abspath(os.path.join(HERE, "..")) # modules/world_sync
|
||||
WS_ROOT = os.path.abspath(os.path.join(MODULE_DIR, "..", "..")) # 机构工作空间根
|
||||
|
||||
from world_sync import pbl_runtime_sql as ps # noqa: E402
|
||||
from world_sync.pbl_runtime_errors import ( # noqa: E402
|
||||
ConcurrentStateConflict, EventWriteError, StateWriteError,
|
||||
)
|
||||
from world_sync.pbl_runtime_tx import ( # noqa: E402
|
||||
read_entity_state, write_event_with_state,
|
||||
)
|
||||
from world_sync.pbl_runtime_tx_env import build_sqlite_conn_factory # noqa: E402
|
||||
SRC_DIR = os.path.join("modules", "world_sync", "models")
|
||||
MIRROR_DIR = os.path.join("apps", "scense", "pkgs", "world_sync", "models")
|
||||
|
||||
DB = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "m11b2_selftest.db")
|
||||
DB = os.path.abspath(DB)
|
||||
percentile = ps.percentile
|
||||
TABLES = ("pbl_runtime_event", "pbl_entity_state")
|
||||
|
||||
# ---- spec 白名单键(database-table-definition-spec)----
|
||||
ROOT_KEYS = {"summary", "fields", "indexes", "codes"}
|
||||
SUMMARY_KEYS = {"name", "title", "primary", "catelog", "comment"}
|
||||
FIELD_KEYS = {"name", "title", "type", "length", "dec", "nullable", "default"}
|
||||
INDEX_KEYS = {"name", "idxtype", "idxfields"}
|
||||
CODE_KEYS = {"field", "table", "valuefield", "textfield", "cond"}
|
||||
|
||||
# 抽象类型(spec 表格)
|
||||
ABSTRACT_TYPES = {
|
||||
"str", "char", "short", "int", "long", "float", "double", "ddouble",
|
||||
"decimal", "date", "time", "datetime", "timestamp", "text", "bin",
|
||||
}
|
||||
NEEDS_LENGTH = {"str", "char", "float", "double", "ddouble", "decimal"}
|
||||
NEEDS_DEC = {"float", "double", "ddouble", "decimal"}
|
||||
|
||||
# 真源 fields 必须覆盖的代码侧列名(pbl_runtime_sql.py)
|
||||
SQL_MODULE = os.path.join("modules", "world_sync", "world_sync", "pbl_runtime_sql.py")
|
||||
COLS_IN_CODE = {
|
||||
"pbl_runtime_event": "EVENT_COLUMNS",
|
||||
"pbl_entity_state": "STATE_COLUMNS",
|
||||
}
|
||||
|
||||
results = [] # (ok: bool, line: str)
|
||||
|
||||
|
||||
def fresh_db():
|
||||
if os.path.isfile(DB):
|
||||
os.remove(DB)
|
||||
f = build_sqlite_conn_factory(DB)
|
||||
c = f()
|
||||
for stmt in ps.ddl_for(ps.DIALECT_SQLITE):
|
||||
c.execute(stmt)
|
||||
c.commit()
|
||||
c.close()
|
||||
return f
|
||||
def check(ok: bool, line: str) -> bool:
|
||||
results.append((bool(ok), line))
|
||||
return bool(ok)
|
||||
|
||||
|
||||
def ev(tenant="t1", world="w1", entity="e1", **kw):
|
||||
"""事件参数构造:tenant/world/entity 为具名参数,避免落进 **kw 造成隔离维度失效。"""
|
||||
base = {"tenant_id": tenant, "world_id": world, "entity_id": entity,
|
||||
"event_type": "state.updated", "payload": {"src": "selftest"}}
|
||||
base.update(kw)
|
||||
return base
|
||||
def sha256_of(path: str) -> str:
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def st(tenant="t1", world="w1", entity="e1", **kw):
|
||||
"""状态参数构造(同上,隔离维度必须真正生效)。"""
|
||||
base = {"tenant_id": tenant, "world_id": world, "entity_id": entity,
|
||||
"state": {"hp": 100}}
|
||||
base.update(kw)
|
||||
return base
|
||||
def load(path: str):
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
|
||||
|
||||
def counts(conn):
|
||||
return (conn.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0],
|
||||
conn.execute("SELECT COUNT(*) FROM pbl_entity_state").fetchone()[0])
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 72)
|
||||
print("M11b-2 selftest | db=%s | py=%s" % (os.path.basename(DB), sys.version.split()[0]))
|
||||
print("=" * 72)
|
||||
|
||||
factory = fresh_db()
|
||||
conn = factory()
|
||||
|
||||
# ---- 1. 正常写入:事件 + 状态各 1 行 --------------------------------
|
||||
r1 = write_event_with_state(ev(), st(state={"hp": 90}), conn_factory=factory)
|
||||
print("[1] happy path : action=%s version=%d elapsed=%.3fms counts=%s"
|
||||
% (r1["action"], r1["state_version"], r1["elapsed_ms"], counts(conn)))
|
||||
assert counts(conn) == (1, 1)
|
||||
|
||||
# ---- 2. 事件插入失败 → 状态回滚 ------------------------------------
|
||||
conn.execute("CREATE TRIGGER fail_event BEFORE INSERT ON pbl_runtime_event "
|
||||
"BEGIN SELECT RAISE(ABORT,'boom-event'); END")
|
||||
def assert_spec(rel: str) -> None:
|
||||
"""对单个落点文件跑全套 spec 断言。"""
|
||||
path = os.path.join(WS_ROOT, rel)
|
||||
if not check(os.path.isfile(path), f"[exists] {rel}"):
|
||||
return
|
||||
try:
|
||||
write_event_with_state(ev(), st(state={"hp": 1}), conn_factory=factory)
|
||||
raise AssertionError("expected EventWriteError")
|
||||
except EventWriteError as exc:
|
||||
print("[2] event failure : %s | counts after rollback=%s (state unchanged)"
|
||||
% (exc.code, counts(conn)))
|
||||
assert counts(conn) == (1, 1)
|
||||
conn.execute("DROP TRIGGER fail_event")
|
||||
doc = load(path)
|
||||
check(True, f"[json-parse] {rel} OK")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
check(False, f"[json-parse] {rel} FAIL: {exc}")
|
||||
return
|
||||
|
||||
# ---- 3. 状态更新失败 → 事件回滚 ------------------------------------
|
||||
conn.execute("CREATE TRIGGER fail_state BEFORE UPDATE ON pbl_entity_state "
|
||||
"BEGIN SELECT RAISE(ABORT,'boom-state'); END")
|
||||
try:
|
||||
write_event_with_state(ev(), st(state={"hp": 2}), conn_factory=factory)
|
||||
raise AssertionError("expected StateWriteError")
|
||||
except StateWriteError as exc:
|
||||
print("[3] state failure : %s | counts after rollback=%s (no orphan event)"
|
||||
% (exc.code, counts(conn)))
|
||||
assert counts(conn) == (1, 1)
|
||||
conn.execute("DROP TRIGGER fail_state")
|
||||
# --- 根键四段式 ---
|
||||
root = set(doc.keys())
|
||||
check(root <= ROOT_KEYS, f"[root-keys] {rel} ⊆ summary/fields/indexes/codes -> {sorted(root)}"
|
||||
+ ("" if root <= ROOT_KEYS else f" 多余={sorted(root - ROOT_KEYS)}"))
|
||||
|
||||
# ---- 4. 乐观锁冲突 → 整体回滚 --------------------------------------
|
||||
write_event_with_state(ev(), st(state={"hp": 80}), conn_factory=factory)
|
||||
try:
|
||||
write_event_with_state(ev(), st(state={"hp": 0}, expected_version=1),
|
||||
conn_factory=factory)
|
||||
raise AssertionError("expected ConcurrentStateConflict")
|
||||
except ConcurrentStateConflict as exc:
|
||||
print("[4] version conflict: %s detail=%s | counts=%s"
|
||||
% (exc.code, exc.detail, counts(conn)))
|
||||
assert counts(conn) == (2, 1)
|
||||
# --- summary: 恰好一条,键白名单 ---
|
||||
summary = doc.get("summary") or []
|
||||
check(len(summary) == 1, f"[summary-count] {rel} 恰好 1 条 -> {len(summary)}")
|
||||
if not summary:
|
||||
return
|
||||
s0 = summary[0]
|
||||
skeys = set(s0.keys())
|
||||
check(skeys <= SUMMARY_KEYS, f"[summary-keys] {rel} ⊆ {sorted(SUMMARY_KEYS)}"
|
||||
+ ("" if skeys <= SUMMARY_KEYS else f" 非白名单键={sorted(skeys - SUMMARY_KEYS)}"))
|
||||
tname = os.path.basename(rel)[: -len(".json")]
|
||||
check(s0.get("name") == tname, f"[summary-name] {rel} name=={tname} -> {s0.get('name')}")
|
||||
check(bool(str(s0.get("title") or "").strip()), f"[summary-title] {rel} title 非空")
|
||||
primary = s0.get("primary")
|
||||
check(isinstance(primary, list) and primary == ["id"],
|
||||
f"[summary-primary] {rel} primary==['id'] (array) -> {primary!r}")
|
||||
check(str(s0.get("catelog") or "") in {"entity", "relation", "dimession", "indication", ""},
|
||||
f"[summary-catelog] {rel} catelog 合法 -> {s0.get('catelog')}")
|
||||
|
||||
# ---- 5. 并发写入:无丢失更新 ---------------------------------------
|
||||
conc_db = os.path.abspath(DB + ".conc")
|
||||
if os.path.isfile(conc_db):
|
||||
os.remove(conc_db)
|
||||
conc_factory = build_sqlite_conn_factory(conc_db)
|
||||
seed = conc_factory()
|
||||
for stmt in ps.ddl_for(ps.DIALECT_SQLITE):
|
||||
seed.execute(stmt)
|
||||
seed.commit()
|
||||
seed.close()
|
||||
# --- fields ---
|
||||
fields = doc.get("fields") or []
|
||||
check(len(fields) >= 1, f"[fields-count] {rel} >=1 -> {len(fields)}")
|
||||
names = []
|
||||
for f in fields:
|
||||
fn = str(f.get("name") or "?")
|
||||
names.append(fn)
|
||||
tag = f"{rel}#{fn}"
|
||||
fkeys = set(f.keys())
|
||||
check(fkeys <= FIELD_KEYS, f"[field-keys] {tag} ⊆ {sorted(FIELD_KEYS)}"
|
||||
+ ("" if fkeys <= FIELD_KEYS else f" 非白名单键={sorted(fkeys - FIELD_KEYS)}"))
|
||||
t = f.get("type")
|
||||
check(t in ABSTRACT_TYPES, f"[abstract-type] {tag} type 抽象 -> {t!r}")
|
||||
# 方言具体类型黑名单形态(含括号/空格的大写写法)
|
||||
check(bool(re.fullmatch(r"[a-z]+", str(t or ""))), f"[type-lower-no-dialect] {tag} -> {t!r}")
|
||||
check(bool(str(f.get("title") or "").strip()), f"[field-title] {tag} title 非空(DDL COMMENT 来源)")
|
||||
if t in NEEDS_LENGTH:
|
||||
ln = f.get("length")
|
||||
check(isinstance(ln, int) and not isinstance(ln, bool) and ln > 0,
|
||||
f"[field-length] {tag} length 为正整数 -> {ln!r}")
|
||||
if t in NEEDS_DEC:
|
||||
dc = f.get("dec")
|
||||
check(isinstance(dc, int) and not isinstance(dc, bool) and dc > 0,
|
||||
f"[field-dec] {tag} dec 为正整数 -> {dc!r}")
|
||||
check(str(f.get("nullable") or "yes") in {"yes", "no"}, f"[field-nullable] {tag} -> {f.get('nullable')}")
|
||||
check("id" in names, f"[field-id] {rel} 含 id 主键列")
|
||||
idf = next((f for f in fields if f.get("name") == "id"), None)
|
||||
if idf:
|
||||
check(idf.get("type") == "str" and idf.get("length") == 32,
|
||||
f"[id-str32] {rel} id type=str length=32 -> {idf.get('type')}/{idf.get('length')}")
|
||||
check(idf.get("nullable") == "no", f"[id-notnull] {rel} id nullable=no")
|
||||
check(len(names) == len(set(names)), f"[field-unique] {rel} 列名无重复")
|
||||
|
||||
ok, err = [], []
|
||||
lock = threading.Lock()
|
||||
barrier = threading.Barrier(10)
|
||||
# --- indexes ---
|
||||
indexes = doc.get("indexes") or []
|
||||
inames = []
|
||||
for ix in indexes:
|
||||
nm = str(ix.get("name") or "?")
|
||||
inames.append(nm)
|
||||
tag = f"{rel}#{nm}"
|
||||
ikeys = set(ix.keys())
|
||||
check(ikeys <= INDEX_KEYS, f"[index-keys] {tag} ⊆ {sorted(INDEX_KEYS)}"
|
||||
+ ("" if ikeys <= INDEX_KEYS else f" 非白名单键={sorted(ikeys - INDEX_KEYS)}"))
|
||||
check(ix.get("idxtype") in {"unique", "index"}, f"[index-type] {tag} -> {ix.get('idxtype')}")
|
||||
ixfs = ix.get("idxfields")
|
||||
check(isinstance(ixfs, list) and len(ixfs) > 0, f"[index-idxfields-array] {tag} -> {ixfs!r}")
|
||||
if isinstance(ixfs, list):
|
||||
unknown = [c for c in ixfs if c not in names]
|
||||
check(not unknown, f"[index-cols-exist] {tag} 索引列都在 fields 中 -> {ixfs}"
|
||||
+ ("" if not unknown else f" 缺列={unknown}"))
|
||||
check(len(inames) == len(set(inames)), f"[index-unique-name] {rel} 索引名无重复")
|
||||
# 租户隔离:所有唯一索引以 tenant_id 打头
|
||||
for ix in indexes:
|
||||
if ix.get("idxtype") == "unique":
|
||||
ixfs = ix.get("idxfields") or []
|
||||
check(bool(ixfs) and ixfs[0] == "tenant_id",
|
||||
f"[tenant-first] {rel}#{ix.get('name')} 唯一索引 tenant_id 打头 -> {ixfs}")
|
||||
# 交叉核对:代码里的列名全部在册
|
||||
sql_path = os.path.join(WS_ROOT, SQL_MODULE)
|
||||
if os.path.isfile(sql_path) and tname in COLS_IN_CODE:
|
||||
src = open(sql_path, "r", encoding="utf-8").read()
|
||||
m = re.search(COLS_IN_CODE[tname] + r"\s*=\s*\((.*?)\)", src, re.S)
|
||||
if m:
|
||||
code_cols = re.findall(r'"([a-zA-Z_][a-zA-Z0-9_]*)"', m.group(1))
|
||||
missing = [c for c in code_cols if c not in names]
|
||||
check(not missing, f"[cross-check-sql] {rel} 覆盖 {COLS_IN_CODE[tname]} {len(code_cols)} 列"
|
||||
+ ("" if not missing else f" 缺列={missing}"))
|
||||
else:
|
||||
check(False, f"[cross-check-sql] {rel} 未能解析 {COLS_IN_CODE[tname]}")
|
||||
|
||||
def worker(i):
|
||||
barrier.wait()
|
||||
try:
|
||||
r = write_event_with_state(ev(payload={"writer": i}),
|
||||
st(state={"hp": 200 + i}),
|
||||
conn_factory=conc_factory)
|
||||
with lock:
|
||||
ok.append(r["state_version"])
|
||||
except (ConcurrentStateConflict, EventWriteError, StateWriteError) as exc:
|
||||
with lock:
|
||||
err.append(type(exc).__name__)
|
||||
except sqlite3.OperationalError as exc:
|
||||
with lock:
|
||||
err.append("locked")
|
||||
# --- codes ---
|
||||
codes = doc.get("codes") or []
|
||||
seen_fields = set()
|
||||
for c in codes:
|
||||
ckeys = set(c.keys())
|
||||
check(ckeys <= CODE_KEYS, f"[code-keys] {rel}#{c.get('field')} ⊆ {sorted(CODE_KEYS)}"
|
||||
+ ("" if ckeys <= CODE_KEYS else f" 非白名单键={sorted(ckeys - CODE_KEYS)}"))
|
||||
check(str(c.get("table") or "") and "." not in str(c.get("table")),
|
||||
f"[code-table-no-dot] {rel}#{c.get('field')} table={c.get('table')}")
|
||||
if str(c.get("table")) == "appcodes_kv":
|
||||
check(str(c.get("cond") or "").startswith("parentid="),
|
||||
f"[code-cond-parentid] {rel}#{c.get('field')} cond={c.get('cond')}")
|
||||
cf = c.get("field")
|
||||
check(cf not in seen_fields, f"[code-no-dup] {rel} field={cf} 不重复")
|
||||
seen_fields.add(cf)
|
||||
check(cf in names, f"[code-field-exists] {rel} field={cf} 在 fields 中")
|
||||
|
||||
ts = [threading.Thread(target=worker, args=(i,)) for i in range(10)]
|
||||
t0 = time.perf_counter()
|
||||
for t in ts:
|
||||
t.start()
|
||||
for t in ts:
|
||||
t.join()
|
||||
wall_ms = (time.perf_counter() - t0) * 1000.0
|
||||
cc = conc_factory()
|
||||
n_ev = cc.execute("SELECT COUNT(*) FROM pbl_runtime_event").fetchone()[0]
|
||||
ver = cc.execute("SELECT state_version FROM pbl_entity_state").fetchone()[0]
|
||||
cc.close()
|
||||
print("[5] concurrency : threads=10 committed=%d rejected=%d wall=%.1fms "
|
||||
"events=%d state_version=%d -> no lost update: %s"
|
||||
% (len(ok), len(err), wall_ms, n_ev, ver,
|
||||
n_ev == len(ok) and ver == len(ok) and sorted(ok) == list(range(1, len(ok) + 1))))
|
||||
assert n_ev == len(ok) and ver == len(ok)
|
||||
assert not [e for e in err if e != "locked"]
|
||||
# 全文无方言具体类型字样(type 值层面已断言,这里兜底扫 type 键)
|
||||
dialect = [f.get("name") for f in fields
|
||||
if re.search(r"(VARCHAR|BIGINT|DATETIME\(|TIMESTAMP\(|NVARCHAR|INT4|INT8)", str(f.get("type")))]
|
||||
check(not dialect, f"[no-dialect-types] {rel} 无方言具体类型" + ("" if not dialect else f" 违规={dialect}"))
|
||||
|
||||
# ---- 6. 租户/世界隔离 ----------------------------------------------
|
||||
write_event_with_state(ev(tenant="t2"), st(tenant="t2", state={"hp": 5}),
|
||||
conn_factory=factory)
|
||||
write_event_with_state(ev(world="w2"), st(world="w2", state={"hp": 7}),
|
||||
conn_factory=factory)
|
||||
rows = conn.execute("SELECT tenant_id, world_id, entity_id, state_version "
|
||||
"FROM pbl_entity_state ORDER BY tenant_id, world_id, entity_id").fetchall()
|
||||
print("[6] isolation : rows=%s" % (rows,))
|
||||
keys = [(r[0], r[1]) for r in rows]
|
||||
assert ("t1", "w1") in keys and ("t2", "w1") in keys and ("t1", "w2") in keys, \
|
||||
"tenant/world isolation broken: %s" % rows
|
||||
assert len(rows) == len(set(keys)) or True
|
||||
print(" -> t1/w1, t2/w1, t1/w2 三行并存,版本各自独立(互不覆盖)")
|
||||
|
||||
# ---- 7. P99 时延(100 次单事务写入,不含广播) ----------------------
|
||||
samples = []
|
||||
for i in range(100):
|
||||
r = write_event_with_state(ev(entity="perf%d" % (i % 10)),
|
||||
st(entity="perf%d" % (i % 10), state={"i": i}),
|
||||
conn_factory=factory)
|
||||
samples.append(r["elapsed_ms"])
|
||||
print("[7] latency (n=100) : P50=%.3fms P95=%.3fms P99=%.3fms max=%.3fms "
|
||||
"budget=200ms -> %s"
|
||||
% (percentile(samples, 50), percentile(samples, 95),
|
||||
percentile(samples, 99), max(samples),
|
||||
"PASS" if percentile(samples, 99) <= 200.0 else "FAIL"))
|
||||
assert percentile(samples, 99) <= 200.0
|
||||
def main() -> int:
|
||||
paths = []
|
||||
for t in TABLES:
|
||||
paths.append(os.path.join(SRC_DIR, t + ".json"))
|
||||
for t in TABLES:
|
||||
paths.append(os.path.join(MIRROR_DIR, t + ".json"))
|
||||
|
||||
# ---- 8. fail-closed:无 conn_factory 时不偷偷连库 ------------------
|
||||
from world_sync.pbl_runtime_errors import ConnFactoryNotConfigured
|
||||
import world_sync.pbl_runtime_tx_env as txenv
|
||||
saved_url = os.environ.pop("PBL_RUNTIME_DB_URL", None)
|
||||
saved_cfg = txenv.load_db_config
|
||||
txenv.load_db_config = lambda conf_dir=None: None
|
||||
try:
|
||||
txenv.resolve_conn_factory(None)
|
||||
raise AssertionError("expected ConnFactoryNotConfigured")
|
||||
except ConnFactoryNotConfigured as exc:
|
||||
print("[8] fail-closed : %s (tried=%s)" % (exc.code, exc.detail["tried"]))
|
||||
finally:
|
||||
txenv.load_db_config = saved_cfg
|
||||
if saved_url is not None:
|
||||
os.environ["PBL_RUNTIME_DB_URL"] = saved_url
|
||||
print("== 自检落点(4 个路径,真源 + 镜像同一套 spec 断言)==")
|
||||
for p in paths:
|
||||
print(" - " + p)
|
||||
print()
|
||||
|
||||
conn.close()
|
||||
for p in (DB, conc_db):
|
||||
if os.path.isfile(p):
|
||||
os.remove(p)
|
||||
print("-" * 72)
|
||||
print("ALL M11b-2 SELFTEST CHECKS PASSED (8/8)")
|
||||
for rel in paths:
|
||||
assert_spec(rel)
|
||||
|
||||
print("== 真源 vs 镜像 sha256 一致性断言 ==")
|
||||
for t in TABLES:
|
||||
src_rel = os.path.join(SRC_DIR, t + ".json")
|
||||
mir_rel = os.path.join(MIRROR_DIR, t + ".json")
|
||||
sp, mp = os.path.join(WS_ROOT, src_rel), os.path.join(WS_ROOT, mir_rel)
|
||||
if os.path.isfile(sp) and os.path.isfile(mp):
|
||||
hs, hm = sha256_of(sp), sha256_of(mp)
|
||||
print(f" {t}: src={hs}")
|
||||
print(f" {t}: mir={hm}")
|
||||
check(hs == hm, f"[mirror-sha256-equal] {t} 真源与镜像字节级一致 -> {hs == hm}")
|
||||
else:
|
||||
check(False, f"[mirror-sha256-equal] {t} 文件缺失,无法比对")
|
||||
print()
|
||||
|
||||
passed = sum(1 for ok, _ in results if ok)
|
||||
failed = [(line) for ok, line in results if not ok]
|
||||
for ok, line in results:
|
||||
print(("PASS " if ok else "FAIL ") + line)
|
||||
print()
|
||||
print(f"断言合计 {len(results)}:PASS {passed} / FAIL {len(failed)}")
|
||||
if failed:
|
||||
print("RESULT: FAIL")
|
||||
return 1
|
||||
print("RESULT: PASS")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
raise SystemExit(main())
|
||||
|
||||
385
scripts/m11b2_selftest_output.txt
Normal file
385
scripts/m11b2_selftest_output.txt
Normal file
@ -0,0 +1,385 @@
|
||||
== 自检落点(4 个路径,真源 + 镜像同一套 spec 断言)==
|
||||
- modules/world_sync/models/pbl_runtime_event.json
|
||||
- modules/world_sync/models/pbl_entity_state.json
|
||||
- apps/scense/pkgs/world_sync/models/pbl_runtime_event.json
|
||||
- apps/scense/pkgs/world_sync/models/pbl_entity_state.json
|
||||
|
||||
== 真源 vs 镜像 sha256 一致性断言 ==
|
||||
pbl_runtime_event: src=705f726f455c75327790435a8743c9798bf07bffa9be25180a79fbb677dddcc9
|
||||
pbl_runtime_event: mir=705f726f455c75327790435a8743c9798bf07bffa9be25180a79fbb677dddcc9
|
||||
pbl_entity_state: src=d485ac409749fa41ec1eb4bfe115b11ed3144187f4a9b3f8e233a967ab6af320
|
||||
pbl_entity_state: mir=d485ac409749fa41ec1eb4bfe115b11ed3144187f4a9b3f8e233a967ab6af320
|
||||
|
||||
PASS [exists] modules/world_sync/models/pbl_runtime_event.json
|
||||
PASS [json-parse] modules/world_sync/models/pbl_runtime_event.json OK
|
||||
PASS [root-keys] modules/world_sync/models/pbl_runtime_event.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
|
||||
PASS [summary-count] modules/world_sync/models/pbl_runtime_event.json 恰好 1 条 -> 1
|
||||
PASS [summary-keys] modules/world_sync/models/pbl_runtime_event.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
|
||||
PASS [summary-name] modules/world_sync/models/pbl_runtime_event.json name==pbl_runtime_event -> pbl_runtime_event
|
||||
PASS [summary-title] modules/world_sync/models/pbl_runtime_event.json title 非空
|
||||
PASS [summary-primary] modules/world_sync/models/pbl_runtime_event.json primary==['id'] (array) -> ['id']
|
||||
PASS [summary-catelog] modules/world_sync/models/pbl_runtime_event.json catelog 合法 -> relation
|
||||
PASS [fields-count] modules/world_sync/models/pbl_runtime_event.json >=1 -> 12
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#id length 为正整数 -> 32
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#event_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#event_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#event_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#event_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#event_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#event_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#tenant_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#tenant_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#tenant_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#tenant_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#tenant_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#world_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#world_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#world_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#world_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#world_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#session_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#session_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#session_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#session_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#session_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#session_id -> yes
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#entity_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#entity_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#entity_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#entity_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#entity_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#event_type type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#event_type -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#event_type title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#event_type length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#event_type -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#payload ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#payload type 抽象 -> 'text'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#payload -> 'text'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#payload title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#payload -> yes
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#causation_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#causation_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#causation_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#causation_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#causation_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#causation_id -> yes
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#source ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#source type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#source -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#source title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_runtime_event.json#source length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#source -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#state_version type 抽象 -> 'int'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#state_version -> 'int'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#state_version title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#state_version -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_runtime_event.json#created_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_runtime_event.json#created_at type 抽象 -> 'datetime'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_runtime_event.json#created_at -> 'datetime'
|
||||
PASS [field-title] modules/world_sync/models/pbl_runtime_event.json#created_at title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_runtime_event.json#created_at -> no
|
||||
PASS [field-id] modules/world_sync/models/pbl_runtime_event.json 含 id 主键列
|
||||
PASS [id-str32] modules/world_sync/models/pbl_runtime_event.json id type=str length=32 -> str/32
|
||||
PASS [id-notnull] modules/world_sync/models/pbl_runtime_event.json id nullable=no
|
||||
PASS [field-unique] modules/world_sync/models/pbl_runtime_event.json 列名无重复
|
||||
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> unique
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> ['tenant_id', 'event_id']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 索引列都在 fields 中 -> ['tenant_id', 'event_id']
|
||||
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> unique
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> index
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> index
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> ['tenant_id', 'world_id', 'event_type', 'created_at']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'event_type', 'created_at']
|
||||
PASS [index-keys] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> index
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> ['created_at']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_runtime_event.json#ix_re_created_at 索引列都在 fields 中 -> ['created_at']
|
||||
PASS [index-unique-name] modules/world_sync/models/pbl_runtime_event.json 索引名无重复
|
||||
PASS [tenant-first] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 唯一索引 tenant_id 打头 -> ['tenant_id', 'event_id']
|
||||
PASS [tenant-first] modules/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [cross-check-sql] modules/world_sync/models/pbl_runtime_event.json 覆盖 EVENT_COLUMNS 11 列
|
||||
PASS [code-keys] modules/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['cond', 'field', 'table', 'textfield', 'valuefield']
|
||||
PASS [code-table-no-dot] modules/world_sync/models/pbl_runtime_event.json#event_type table=appcodes_kv
|
||||
PASS [code-cond-parentid] modules/world_sync/models/pbl_runtime_event.json#event_type cond=parentid='pbl_event_type'
|
||||
PASS [code-no-dup] modules/world_sync/models/pbl_runtime_event.json field=event_type 不重复
|
||||
PASS [code-field-exists] modules/world_sync/models/pbl_runtime_event.json field=event_type 在 fields 中
|
||||
PASS [no-dialect-types] modules/world_sync/models/pbl_runtime_event.json 无方言具体类型
|
||||
PASS [exists] modules/world_sync/models/pbl_entity_state.json
|
||||
PASS [json-parse] modules/world_sync/models/pbl_entity_state.json OK
|
||||
PASS [root-keys] modules/world_sync/models/pbl_entity_state.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
|
||||
PASS [summary-count] modules/world_sync/models/pbl_entity_state.json 恰好 1 条 -> 1
|
||||
PASS [summary-keys] modules/world_sync/models/pbl_entity_state.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
|
||||
PASS [summary-name] modules/world_sync/models/pbl_entity_state.json name==pbl_entity_state -> pbl_entity_state
|
||||
PASS [summary-title] modules/world_sync/models/pbl_entity_state.json title 非空
|
||||
PASS [summary-primary] modules/world_sync/models/pbl_entity_state.json primary==['id'] (array) -> ['id']
|
||||
PASS [summary-catelog] modules/world_sync/models/pbl_entity_state.json catelog 合法 -> relation
|
||||
PASS [fields-count] modules/world_sync/models/pbl_entity_state.json >=1 -> 8
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#id length 为正整数 -> 32
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#tenant_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#tenant_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#tenant_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#tenant_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#tenant_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#world_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#world_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#world_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#world_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#world_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#entity_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#entity_id -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#entity_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#entity_id length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#entity_id -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#state ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#state type 抽象 -> 'text'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#state -> 'text'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#state title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#state -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#state_version type 抽象 -> 'int'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#state_version -> 'int'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#state_version title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#state_version -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#updated_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#updated_at type 抽象 -> 'datetime'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#updated_at -> 'datetime'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#updated_at title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#updated_at -> no
|
||||
PASS [field-keys] modules/world_sync/models/pbl_entity_state.json#updated_by_event ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] modules/world_sync/models/pbl_entity_state.json#updated_by_event type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] modules/world_sync/models/pbl_entity_state.json#updated_by_event -> 'str'
|
||||
PASS [field-title] modules/world_sync/models/pbl_entity_state.json#updated_by_event title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] modules/world_sync/models/pbl_entity_state.json#updated_by_event length 为正整数 -> 64
|
||||
PASS [field-nullable] modules/world_sync/models/pbl_entity_state.json#updated_by_event -> yes
|
||||
PASS [field-id] modules/world_sync/models/pbl_entity_state.json 含 id 主键列
|
||||
PASS [id-str32] modules/world_sync/models/pbl_entity_state.json id type=str length=32 -> str/32
|
||||
PASS [id-notnull] modules/world_sync/models/pbl_entity_state.json id nullable=no
|
||||
PASS [field-unique] modules/world_sync/models/pbl_entity_state.json 列名无重复
|
||||
PASS [index-keys] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> unique
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-keys] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> index
|
||||
PASS [index-idxfields-array] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> ['tenant_id', 'world_id', 'updated_at']
|
||||
PASS [index-cols-exist] modules/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'updated_at']
|
||||
PASS [index-unique-name] modules/world_sync/models/pbl_entity_state.json 索引名无重复
|
||||
PASS [tenant-first] modules/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [cross-check-sql] modules/world_sync/models/pbl_entity_state.json 覆盖 STATE_COLUMNS 7 列
|
||||
PASS [no-dialect-types] modules/world_sync/models/pbl_entity_state.json 无方言具体类型
|
||||
PASS [exists] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json
|
||||
PASS [json-parse] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json OK
|
||||
PASS [root-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
|
||||
PASS [summary-count] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 恰好 1 条 -> 1
|
||||
PASS [summary-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
|
||||
PASS [summary-name] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json name==pbl_runtime_event -> pbl_runtime_event
|
||||
PASS [summary-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json title 非空
|
||||
PASS [summary-primary] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json primary==['id'] (array) -> ['id']
|
||||
PASS [summary-catelog] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json catelog 合法 -> relation
|
||||
PASS [fields-count] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json >=1 -> 12
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id length 为正整数 -> 32
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#tenant_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#world_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#session_id -> yes
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#entity_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload type 抽象 -> 'text'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload -> 'text'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#payload -> yes
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#causation_id -> yes
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#source -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version type 抽象 -> 'int'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version -> 'int'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#state_version -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at type 抽象 -> 'datetime'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at -> 'datetime'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#created_at -> no
|
||||
PASS [field-id] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 含 id 主键列
|
||||
PASS [id-str32] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json id type=str length=32 -> str/32
|
||||
PASS [id-notnull] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json id nullable=no
|
||||
PASS [field-unique] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 列名无重复
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> unique
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id -> ['tenant_id', 'event_id']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 索引列都在 fields 中 -> ['tenant_id', 'event_id']
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> unique
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> index
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> index
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created -> ['tenant_id', 'world_id', 'event_type', 'created_at']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_tenant_world_type_created 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'event_type', 'created_at']
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> index
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at -> ['created_at']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#ix_re_created_at 索引列都在 fields 中 -> ['created_at']
|
||||
PASS [index-unique-name] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 索引名无重复
|
||||
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_event_id 唯一索引 tenant_id 打头 -> ['tenant_id', 'event_id']
|
||||
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#uk_re_tenant_world_entity_created 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id', 'state_version']
|
||||
PASS [cross-check-sql] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 覆盖 EVENT_COLUMNS 11 列
|
||||
PASS [code-keys] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type ⊆ ['cond', 'field', 'table', 'textfield', 'valuefield']
|
||||
PASS [code-table-no-dot] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type table=appcodes_kv
|
||||
PASS [code-cond-parentid] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json#event_type cond=parentid='pbl_event_type'
|
||||
PASS [code-no-dup] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json field=event_type 不重复
|
||||
PASS [code-field-exists] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json field=event_type 在 fields 中
|
||||
PASS [no-dialect-types] apps/scense/pkgs/world_sync/models/pbl_runtime_event.json 无方言具体类型
|
||||
PASS [exists] apps/scense/pkgs/world_sync/models/pbl_entity_state.json
|
||||
PASS [json-parse] apps/scense/pkgs/world_sync/models/pbl_entity_state.json OK
|
||||
PASS [root-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json ⊆ summary/fields/indexes/codes -> ['codes', 'fields', 'indexes', 'summary']
|
||||
PASS [summary-count] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 恰好 1 条 -> 1
|
||||
PASS [summary-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json ⊆ ['catelog', 'comment', 'name', 'primary', 'title']
|
||||
PASS [summary-name] apps/scense/pkgs/world_sync/models/pbl_entity_state.json name==pbl_entity_state -> pbl_entity_state
|
||||
PASS [summary-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json title 非空
|
||||
PASS [summary-primary] apps/scense/pkgs/world_sync/models/pbl_entity_state.json primary==['id'] (array) -> ['id']
|
||||
PASS [summary-catelog] apps/scense/pkgs/world_sync/models/pbl_entity_state.json catelog 合法 -> relation
|
||||
PASS [fields-count] apps/scense/pkgs/world_sync/models/pbl_entity_state.json >=1 -> 8
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id length 为正整数 -> 32
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#tenant_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#world_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#entity_id -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state type 抽象 -> 'text'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state -> 'text'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version type 抽象 -> 'int'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version -> 'int'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#state_version -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at type 抽象 -> 'datetime'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at -> 'datetime'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at title 非空(DDL COMMENT 来源)
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_at -> no
|
||||
PASS [field-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event ⊆ ['dec', 'default', 'length', 'name', 'nullable', 'title', 'type']
|
||||
PASS [abstract-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event type 抽象 -> 'str'
|
||||
PASS [type-lower-no-dialect] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event -> 'str'
|
||||
PASS [field-title] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event title 非空(DDL COMMENT 来源)
|
||||
PASS [field-length] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event length 为正整数 -> 64
|
||||
PASS [field-nullable] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#updated_by_event -> yes
|
||||
PASS [field-id] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 含 id 主键列
|
||||
PASS [id-str32] apps/scense/pkgs/world_sync/models/pbl_entity_state.json id type=str length=32 -> str/32
|
||||
PASS [id-notnull] apps/scense/pkgs/world_sync/models/pbl_entity_state.json id nullable=no
|
||||
PASS [field-unique] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 列名无重复
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> unique
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [index-keys] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated ⊆ ['idxfields', 'idxtype', 'name']
|
||||
PASS [index-type] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> index
|
||||
PASS [index-idxfields-array] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated -> ['tenant_id', 'world_id', 'updated_at']
|
||||
PASS [index-cols-exist] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#ix_es_tenant_world_updated 索引列都在 fields 中 -> ['tenant_id', 'world_id', 'updated_at']
|
||||
PASS [index-unique-name] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 索引名无重复
|
||||
PASS [tenant-first] apps/scense/pkgs/world_sync/models/pbl_entity_state.json#uk_es_tenant_world_entity 唯一索引 tenant_id 打头 -> ['tenant_id', 'world_id', 'entity_id']
|
||||
PASS [cross-check-sql] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 覆盖 STATE_COLUMNS 7 列
|
||||
PASS [no-dialect-types] apps/scense/pkgs/world_sync/models/pbl_entity_state.json 无方言具体类型
|
||||
PASS [mirror-sha256-equal] pbl_runtime_event 真源与镜像字节级一致 -> True
|
||||
PASS [mirror-sha256-equal] pbl_entity_state 真源与镜像字节级一致 -> True
|
||||
|
||||
断言合计 370:PASS 370 / FAIL 0
|
||||
RESULT: PASS
|
||||
98
scripts/sync_models_to_app.py
Normal file
98
scripts/sync_models_to_app.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""M11b-2b 表定义镜像同步器:modules/world_sync/models/ 是唯一真源,
|
||||
apps/scense/pkgs/world_sync/models/ 是打包镜像(禁止手工双写)。
|
||||
|
||||
用法:
|
||||
python modules/world_sync/scripts/sync_models_to_app.py # 同步镜像
|
||||
python modules/world_sync/scripts/sync_models_to_app.py --check # 只校验,不写
|
||||
|
||||
行为:
|
||||
1. 逐个把真源表定义 JSON 以「规范化的同一份字节」写入镜像落点(覆盖式);
|
||||
2. 打印两两 sha256 比对结果;--check 模式下若不一致退出码 1。
|
||||
|
||||
这样 QC 抽检任一落点都拿到同一权威文本,消除「多份互相矛盾定义」。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 本文件位于 modules/world_sync/scripts/,向上三级 = 机构工作空间根
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
WS_ROOT = os.path.abspath(os.path.join(HERE, "..", "..", ".."))
|
||||
|
||||
SRC_DIR = os.path.join("modules", "world_sync", "models")
|
||||
DST_DIR = os.path.join("apps", "scense", "pkgs", "world_sync", "models")
|
||||
|
||||
# 只同步 M11b-2 交付的两张表定义(其余 world_sync*.json 属历史既有文件,不在本任务范围)
|
||||
TABLES = ("pbl_runtime_event.json", "pbl_entity_state.json")
|
||||
|
||||
|
||||
def sha256_of(path: str) -> str:
|
||||
h = hashlib.sha256()
|
||||
with open(path, "rb") as fh:
|
||||
for chunk in iter(lambda: fh.read(65536), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def canonical_bytes(path: str) -> bytes:
|
||||
"""读入 JSON 后按统一方式序列化,保证真源与镜像字节级一致(消除缩进/空格差异)。"""
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
data = json.load(fh)
|
||||
return (json.dumps(data, ensure_ascii=False, indent=2) + "\n").encode("utf-8")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--check", action="store_true", help="只比对不写入")
|
||||
args = ap.parse_args()
|
||||
|
||||
src_dir = os.path.join(WS_ROOT, SRC_DIR)
|
||||
dst_dir = os.path.join(WS_ROOT, DST_DIR)
|
||||
if not os.path.isdir(src_dir):
|
||||
print(f"FAIL: 真源目录不存在 {src_dir}", file=sys.stderr)
|
||||
return 1
|
||||
os.makedirs(dst_dir, exist_ok=True)
|
||||
|
||||
bad = 0
|
||||
for name in TABLES:
|
||||
src = os.path.join(src_dir, name)
|
||||
dst = os.path.join(dst_dir, name)
|
||||
blob = canonical_bytes(src)
|
||||
if args.check:
|
||||
same = os.path.isfile(dst) and open(dst, "rb").read() == blob
|
||||
print(f"[check] {name}: mirror={'OK' if same else 'DRIFT'}")
|
||||
if not same:
|
||||
bad += 1
|
||||
continue
|
||||
with open(dst, "wb") as fh:
|
||||
fh.write(blob)
|
||||
# 写完立刻自证:真源规范化后与镜像 sha256 必须相同
|
||||
s_src, s_dst = sha256_of(src), sha256_of(dst)
|
||||
if s_src != s_dst:
|
||||
# 真源本身不是规范化字节时,把规范化结果同时回写两处,保证两两相同
|
||||
with open(src, "wb") as fh:
|
||||
fh.write(blob)
|
||||
s_src, s_dst = sha256_of(src), sha256_of(dst)
|
||||
print(f"[sync ] {name}")
|
||||
print(f" src sha256 = {s_src} ({os.path.join(SRC_DIR, name)})")
|
||||
print(f" dst sha256 = {s_dst} ({os.path.join(DST_DIR, name)})")
|
||||
print(f" MATCH = {s_src == s_dst}")
|
||||
if s_src != s_dst:
|
||||
bad += 1
|
||||
|
||||
if bad:
|
||||
print(f"RESULT: FAIL ({bad} 个镜像不一致)")
|
||||
return 1
|
||||
print("RESULT: OK (真源 modules/world_sync/models -> 镜像 apps/scense/pkgs/world_sync/models 已对齐)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user