pbl_blueprint/tools/patch_m1b.py
2026-09-16 17:17:53 +08:00

380 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

# -*- coding: utf-8 -*-
"""M1b 交付补丁脚本(一次性执行):
1) m1b_init.py表模型路径改指 models/m1b/*.json四段式CRUD 定义指 json/m1b/*.json
DDL 路径改指仓库根 sql/m1b_ddl.sql并接入真实库执行通道m1b_db.SorDB
2) __init__.py补 M1b 导出三处同步之「__init__ 导出」环节)。
3) init.py补 M1b env 注册三处同步之「init.py env 注册」环节)。
执行python3 tools/patch_m1b.py
"""
import io
import os
import sys
REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PKG = os.path.join(REPO, "pbl_blueprint")
def read(p):
with io.open(p, "r", encoding="utf-8") as f:
return f.read()
def write(p, s):
with io.open(p, "w", encoding="utf-8") as f:
f.write(s)
print("WROTE %s (%d chars)" % (os.path.relpath(p, REPO), len(s)))
def sub_once(text, old, new, tag):
if old not in text:
raise SystemExit("PATCH FAIL [%s]: anchor not found" % tag)
if text.count(old) != 1:
raise SystemExit("PATCH FAIL [%s]: anchor not unique (%d)" % (tag, text.count(old)))
return text.replace(old, new)
# ---------------------------------------------------------------- 1) m1b_init.py
p = os.path.join(PKG, "m1b_init.py")
t = read(p)
old_head = '''_HERE = os.path.dirname(os.path.abspath(__file__))
M1B_MODEL_FILES = [
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_template.json"),
os.path.join(_HERE, "json", "m1b", "pbl_ext_field_def.json"),
os.path.join(_HERE, "json", "m1b", "pbl_subobject_ext.json"),
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_ref.json"),
]
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
"pbl_blueprint_ref"]
M1B_DDL = os.path.join(_HERE, "sql", "m1b_ddl.sql")
'''
new_head = '''# 目录规约module-development-spec / database-table-definition-spec
# * 表定义(四段式 summary/fields/indexes/codes→ pbl_blueprint/models/m1b/*.json
# * CRUD 定义tblname + params.editable → pbl_blueprint/json/m1b/*.json
# * 幂等 DDL4 表 CREATE TABLE IF NOT EXISTS → <repo>/sql/m1b_ddl.sql
_HERE = os.path.dirname(os.path.abspath(__file__)) # .../pbl_blueprint/pbl_blueprint
_REPO = os.path.dirname(_HERE) # .../pbl_blueprint仓库根
M1B_TABLES = ["pbl_blueprint_template", "pbl_ext_field_def", "pbl_subobject_ext",
"pbl_blueprint_ref"]
# 表模型(四段式)——建表/字段校验的权威来源
M1B_MODEL_FILES = [
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_template.json"),
os.path.join(_HERE, "models", "m1b", "pbl_ext_field_def.json"),
os.path.join(_HERE, "models", "m1b", "pbl_subobject_ext.json"),
os.path.join(_HERE, "models", "m1b", "pbl_blueprint_ref.json"),
]
# CRUD 定义tblname + params.editable——供 sqlor/前端 CRUD 装配
M1B_CRUD_FILES = [
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_template.json"),
os.path.join(_HERE, "json", "m1b", "pbl_ext_field_def.json"),
os.path.join(_HERE, "json", "m1b", "pbl_subobject_ext.json"),
os.path.join(_HERE, "json", "m1b", "pbl_blueprint_ref.json"),
]
M1B_DDL = os.path.join(_REPO, "sql", "m1b_ddl.sql")
'''
t = sub_once(t, old_head, new_head, "m1b_init head")
t = sub_once(
t,
'__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "init_m1b", "ensure_m1b_tables",\n'
' "ensure_platform_ext_defs", "seed_platform_data"]',
'__all__ = ["M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",\n'
' "PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",\n'
' "init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",\n'
' "seed_platform_data", "load_m1b_models", "load_m1b_crud_defs"]',
"m1b_init __all__")
# 建表接入真实库通道m1b_db.SorDB.sqlExe并做 Q-OPEN-3 闸门保护
old_ensure = ''' result = {"ok": True, "models": [m.get("table") for m in models], "ddl": None, "executed": False}
'''
new_ensure = ''' result = {"ok": True, "models": [m.get("table") for m in models],
"ddl": None, "executed": 0, "statements": 0, "errors": []}
'''
t = sub_once(t, old_ensure, new_ensure, "ensure result init")
old_ddl = ''' # 2) DDL 兜底
if os.path.exists(M1B_DDL):
try:
with open(M1B_DDL, "r", encoding="utf-8") as fp:
ddl = fp.read()
result["ddl"] = M1B_DDL
if callable(executor):
for stmt in [s.strip() for s in ddl.split(";") if s.strip()
and not s.strip().startswith("--")]:
try:
executor(stmt)
except Exception:
continue
result["executed"] = True
except Exception:
pass
return result
'''
new_ddl = ''' # 2) 幂等 DDLsql/m1b_ddl.sql4 表 CREATE TABLE IF NOT EXISTS
if os.path.exists(M1B_DDL):
try:
with open(M1B_DDL, "r", encoding="utf-8") as fp:
ddl = fp.read()
result["ddl"] = M1B_DDL
from .m1b_db import split_ddl, guard_sql
stmts = split_ddl(ddl)
result["statements"] = len(stmts)
# 执行通道优先级:显式 executor > 真实库 SorDBsqlor 标准 API
runner = None
if callable(executor):
runner = executor
else:
try:
from .m1b_db import make_db
real = make_db()
if real is not None:
runner = real.sqlExe
result["channel"] = "sqlor(SorDB.sqlExe)"
except Exception:
runner = None
if runner is not None:
for stmt in stmts:
try:
guard_sql(stmt) # Q-OPEN-3拦截任何复用域基表写/改
runner(stmt)
result["executed"] += 1
except Exception as e: # 单条失败不阻断其余(幂等重跑可补)
result["errors"].append("%s: %s" % (type(e).__name__, e))
result["ok"] = not result["errors"]
else:
result["channel"] = "none(内存适配器:表按需自动创建)"
except Exception as e:
result["ok"] = False
result["errors"].append("ddl_load: %s" % e)
return result
def load_m1b_models():
"""读取 4 张表的四段式表模型models/m1b/*.json"""
out = []
for f in M1B_MODEL_FILES:
if not os.path.exists(f):
continue
try:
with open(f, "r", encoding="utf-8") as fp:
d = json.load(fp)
d["_file"] = os.path.relpath(f, _REPO)
out.append(d)
except Exception:
continue
return out
def load_m1b_crud_defs():
"""读取 4 张表的 CRUD 定义json/m1b/*.jsontblname + params.editable"""
out = []
for f in M1B_CRUD_FILES:
if not os.path.exists(f):
continue
try:
with open(f, "r", encoding="utf-8") as fp:
d = json.load(fp)
d["_file"] = os.path.relpath(f, _REPO)
out.append(d)
except Exception:
continue
return out
'''
t = sub_once(t, old_ddl, new_ddl, "ensure ddl block")
# init_m1b 返回值补 CRUD 定义装载证据
t = sub_once(
t,
''' return {"ok": True, "milestone": "M1b", "tables": tables, "routes": routes,
"seed": seeded, "q_open_3": "world/scene/entity 基表零改动"}''',
''' return {"ok": True, "milestone": "M1b", "tables": tables, "routes": routes,
"seed": seeded,
"models_loaded": [m.get("_file") for m in load_m1b_models()],
"crud_defs_loaded": [c.get("tblname") for c in load_m1b_crud_defs()],
"q_open_3": "world/scene/entity/script 基表零改动、零加列、零回写"}''',
"init_m1b return")
write(p, t)
# ---------------------------------------------------------------- 2) __init__.py
p = os.path.join(PKG, "__init__.py")
t = read(p)
old = '''# ------------------------------------------------------------------ 挂载入口
from .init import load_pbl_blueprint # noqa: F401
'''
new = '''# ------------------------------------------------------------------ 挂载入口
from .init import load_pbl_blueprint # noqa: F401
# ------------------------------------------------------------------ M1b模板平台公共部分 / 子对象扩展 / 关联表判定)
# 三处同步之「__init__ 导出」环节:函数定义(m1b_*.py) + 本文件导出 + init.py env 注册
from .m1b_init import ( # noqa: F401
M1B_TABLES,
M1B_MODEL_FILES,
M1B_CRUD_FILES,
M1B_DDL,
PLATFORM_EXT_DEFS,
PLATFORM_TEMPLATES,
init_m1b,
ensure_m1b_tables,
ensure_platform_ext_defs,
seed_platform_data,
load_m1b_models,
load_m1b_crud_defs,
)
from .m1b_api import M1B_ROUTES, dispatch as m1b_dispatch, register_m1b_routes # noqa: F401
from .m1b_db import ( # noqa: F401
PLATFORM_TENANT_KEY,
WRITE_PROTECTED_TABLES,
PBL_DB_WRITE_PROTECTED,
WriteProtectedError,
SorDB,
tenant_key as m1b_tenant_key,
assert_writable as m1b_assert_writable,
make_db as m1b_make_db,
)
from . import m1b_template # noqa: F401
from . import m1b_subobject # noqa: F401
from . import m1b_ref # noqa: F401
'''
t = sub_once(t, old, new, "__init__ imports")
t = sub_once(
t,
''' # 挂载入口
"load_pbl_blueprint",
]''',
''' # 挂载入口
"load_pbl_blueprint",
# ---- M1b挂载/建表/种子
"M1B_TABLES", "M1B_MODEL_FILES", "M1B_CRUD_FILES", "M1B_DDL",
"PLATFORM_EXT_DEFS", "PLATFORM_TEMPLATES",
"init_m1b", "ensure_m1b_tables", "ensure_platform_ext_defs",
"seed_platform_data", "load_m1b_models", "load_m1b_crud_defs",
# ---- M1b路由
"M1B_ROUTES", "m1b_dispatch", "register_m1b_routes",
# ---- M1b真实库适配 + Q-OPEN-3 写保护闸门
"PLATFORM_TENANT_KEY", "WRITE_PROTECTED_TABLES", "PBL_DB_WRITE_PROTECTED",
"WriteProtectedError", "SorDB", "m1b_tenant_key", "m1b_assert_writable",
"m1b_make_db",
# ---- M1b业务子模块
"m1b_template", "m1b_subobject", "m1b_ref",
]''',
"__init__ __all__")
t = sub_once(
t, '__version__ = "1.0.0"', '__version__ = "1.1.0" # M1b模板平台公共部分/子对象扩展/关联表',
"__init__ version")
write(p, t)
# ---------------------------------------------------------------- 3) init.py
p = os.path.join(PKG, "init.py")
t = read(p)
old = '''MODULE_NAME = "pbl_blueprint"
TABLE_NAMES = list(_tables.TABLES.keys()) # 11 表
SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类
'''
new = '''from . import m1b_init as _m1b_init
from .m1b_api import M1B_ROUTES, register_m1b_routes
from .m1b_init import (M1B_CRUD_FILES, M1B_DDL, M1B_MODEL_FILES, M1B_TABLES,
ensure_m1b_tables, init_m1b, load_m1b_crud_defs,
load_m1b_models, seed_platform_data)
MODULE_NAME = "pbl_blueprint"
TABLE_NAMES = list(_tables.TABLES.keys()) # M1a 11 表
SUBOBJECT_TYPES = _tables.SUBOBJECT_TYPES # 7 类
M1B_TABLE_NAMES = list(M1B_TABLES) # M1b 4 表
ALL_TABLE_NAMES = TABLE_NAMES + [t for t in M1B_TABLE_NAMES if t not in TABLE_NAMES]
'''
t = sub_once(t, old, new, "init.py imports")
t = sub_once(
t,
'''__all__ = [
"load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",
"SUBOBJECT_TYPES", "BUILTIN_SCHEMA",''',
'''__all__ = [
"load_pbl_blueprint", "ensure_tables", "MODULE_NAME", "TABLE_NAMES",
"SUBOBJECT_TYPES", "BUILTIN_SCHEMA",
# ---- M1b 三处同步之「init.py env 注册」环节
"M1B_TABLES", "M1B_TABLE_NAMES", "ALL_TABLE_NAMES", "M1B_MODEL_FILES",
"M1B_CRUD_FILES", "M1B_DDL", "M1B_ROUTES",
"init_m1b", "ensure_m1b_tables", "register_m1b_routes",
"load_m1b_models", "load_m1b_crud_defs", "seed_platform_data",''',
"init.py __all__")
old_load = '''def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入
if ensure:
ensure_tables(sor)
if app is not None:
try:
app.register_module(MODULE_NAME, {
"tables": TABLE_NAMES,'''
new_load = '''def _m1b_register_callback(app):
"""解析应用层路由注册回调register_route / register / route 三选一。"""
if app is None:
return None
for name in ("register_route", "register", "route", "add_route"):
fn = getattr(app, name, None)
if callable(fn):
return fn
return None
def load_pbl_blueprint(app=None, sor=None, ensure=False, **kw):
dbname = get_module_dbname(_MODULE_NAME) # noqa: F841 库名由应用注入
if ensure:
ensure_tables(sor)
# ------------------------------------------------------------ M1b 挂载
# 三处同步m1b_api.register_m1b_routes定义+ __init__.py导出+ 此处env 注册)
# Q-OPEN-3init_m1b 内部只建 M1b 自有 4 表,绝不触碰 world/scene/entity/script 基表。
m1b_result = {"ok": False, "skipped": True}
if kw.get("m1b", True):
try:
m1b_result = init_m1b(
db=kw.get("m1b_db"),
env=kw.get("env"),
register=_m1b_register_callback(app),
executor=kw.get("m1b_executor"),
seed=bool(kw.get("m1b_seed", ensure)),
)
except Exception as e: # M1b 失败不拖垮 M1a 挂载
m1b_result = {"ok": False, "error": "%s: %s" % (type(e).__name__, e)}
if app is not None:
try:
app.register_module(MODULE_NAME, {
"tables": ALL_TABLE_NAMES,
"m1b": {
"tables": M1B_TABLE_NAMES,
"model_files": M1B_MODEL_FILES,
"crud_files": M1B_CRUD_FILES,
"ddl": M1B_DDL,
"routes": [(m, p) for m, p, _ in M1B_ROUTES],
"init": m1b_result,
},''',
t = sub_once(t, old_load, new_load, "init.py load_pbl_blueprint")
t = sub_once(
t,
''' return {"module": MODULE_NAME, "tables": TABLE_NAMES,
"subobject_types": list(SUBOBJECT_TYPES), "loaded": True}''',
''' return {"module": MODULE_NAME, "tables": TABLE_NAMES,
"m1b_tables": M1B_TABLE_NAMES, "m1b": m1b_result,
"subobject_types": list(SUBOBJECT_TYPES), "loaded": True}''',
"init.py return")
write(p, t)
print("PATCH OK")