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

This commit is contained in:
agent.develop 2026-09-19 22:24:59 +08:00
parent 7f0f5609d3
commit ad744aaf06
4 changed files with 198 additions and 69 deletions

View File

@ -5,9 +5,7 @@
DELIMITER $$
-- append-only 断言过程:非白名单操作一律 SIGNAL 拒绝
DROP PROCEDURE IF EXISTS `pbl_assert_append_only`;
CREATE$$
DROP PROCEDURE IF EXISTS `pbl_assert_append_only`$$
CREATE PROCEDURE `pbl_assert_append_only`(
IN p_table VARCHAR(64), IN p_priv VARCHAR(16), IN p_detail VARCHAR(255))
@ -22,13 +20,11 @@ BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED',
MYSQL_ERRNO = 45001;
END;
END$$
-- BEFORE UPDATE 护栏:把违规语句原文带进错误消息,便于定位调用方
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_upd`$$
CREATE$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_upd`
BEFORE UPDATE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
@ -50,13 +46,11 @@ BEGIN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_UPDATE',
MYSQL_ERRNO = 45002;
END IF;
END;
END$$
-- BEFORE DELETE 护栏
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_del`$$
CREATE$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_del`
BEFORE DELETE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
@ -78,10 +72,10 @@ BEGIN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_DELETE',
MYSQL_ERRNO = 45003;
END IF;
END;
END$$
-- 权限层加固(可选,需 DBA 账号执行app 账号只保留增/查)
-- GRANT INSERT, SELECT, UPDATE, DELETE ON `pbls`.`pbl_runtime_event` TO NULL; -- 占位,勿执行
-- REVOKE UPDATE, DELETE ON `pbls`.`pbl_runtime_event` FROM 'pbls'@'%'$$
-- REVOKE UPDATE, DELETE ON `pbls`.`pbl_runtime_event` FROM 'pbls'@'%';
DELIMITER ;

View File

@ -5,9 +5,7 @@
DELIMITER $$
-- 1) 幂等补建未来月分区pmax 存在时用 REORGANIZE PARTITION 拆出新月份(不丢数据)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_ensure_partitions`;
CREATE$$
DROP PROCEDURE IF EXISTS `pbl_runtime_event_ensure_partitions`$$
CREATE PROCEDURE `pbl_runtime_event_ensure_partitions`(IN p_months_ahead INT)
MODIFIES SQL DATA
@ -49,13 +47,11 @@ BEGIN
END IF;
END LOOP;
CLOSE cur;
END;
END$$
-- 2) 到期归档:整月 DROP PARTITIONappend-only 表唯一合规的清理手段)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_prune_partitions`$$
CREATE$$
CREATE PROCEDURE `pbl_runtime_event_prune_partitions`(IN p_keep_months INT)
MODIFIES SQL DATA
BEGIN
@ -82,16 +78,11 @@ BEGIN
DEALLOCATE PREPARE pbl_drop_st;
END LOOP;
CLOSE cur;
END;
END$$
-- 3) 每月自动补分区(需 event_scheduler=ON否则用 cron见开发说明
-- SET GLOBAL event_scheduler = ON$$
DROP$$
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`;
CREATE$$
-- SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`$$
CREATE EVENT IF NOT EXISTS `ev_pbl_runtime_event_partition_maintain`
ON SCHEDULE EVERY 1 MONTH

View File

@ -384,23 +384,140 @@ def full_script(dbname="pbls", app_user="pbls", today=None, months=None):
+ migrate_from_legacy_sql())
def _with_delimiter(body):
"""把过程/触发器体内语句交给 mysql CLI整段以 $$ 结束。"""
_BLOCK_OPEN = re.compile(r"^BEGIN\b", re.I)
_BLOCK_CLOSE = re.compile(r"^END\s*;?\s*$", re.I)
_SUB_CLOSE = re.compile(r"^END\s+(IF|LOOP|CASE)\b", re.I)
_STMT_HEAD = re.compile(
r"^(DROP|CREATE|GRANT|REVOKE|SET|SELECT|CALL|ALTER|WITH|INSERT|UPDATE|DELETE|START)\b", re.I)
# 一个 $$ 段内不应再出现的「另一条 DDL 语句头」——出现即说明两条语句被黏在了一起
_INLINE_DDL_HEAD = re.compile(
r"^\s*(DROP|CREATE)\s+(PROCEDURE|TRIGGER|EVENT|TABLE|FUNCTION)\b", re.I)
# 裸关键字残缺行:`CREATE$$` / `DROP$$`QC #12 的致命产物形态)
_BARE_TOKEN = re.compile(r"^(CREATE|DROP|ALTER|GRANT|REVOKE)\s*\$\$\s*$", re.I)
def _with_delimiter(body, delim="$$"):
"""把过程/触发器/EVENT 正文按语句切分,逐条以 $$ 收尾,交给 mysql CLIDELIMITER执行。
QC #12 修复(致命缺陷:迁移资产整体不可执行):
旧实现 `re.split(r"(?<=;)\n(?=(DROP|CREATE|...))", body)` 的前瞻里带**捕获组**
re.split 会把捕获到的文本'CREATE' / 'DROP'作为独立元素插进结果列表这些孤立 token
随后被当成一条完整语句拼成裸 `CREATE$$` / `DROP$$` 行写进 .sql `DELIMITER $$`
服务端把 `CREATE` 当独立语句解析 ERROR 106402/03/canonical 三个文件全部无法执行
另外旧实现把 `DROP PROCEDURE/TRIGGER/EVENT` 留在 `;` 结尾 DELIMITER 已改成 `$$`
客户端只认 `$$` 为语句结束符`;` 结尾的 DROP 会与下一条 CREATE 黏成一条语句
现改为按行扫描 + BEGIN/END 嵌套深度判定语句边界不再使用带捕获组的 re.split
· 每条语句去掉结尾 `;` 后以 `$$` 结束 DROP ... IF EXISTS
· `--` 注释行原样保留不加 `$$`否则 `$$` 被行注释吞掉语句永不结束
· 结构上只可能产出以完整关键字开头的语句段不可能产出裸 `CREATE`/`DROP`
"""
out = []
for chunk in re.split(r"(?<=;)\n(?=(DROP|CREATE|GRANT|REVOKE|SELECT|--|SET))", body):
c = chunk.strip()
if not c:
buf = []
cmts = []
depth = [0]
def flush():
if not buf:
if cmts:
out.append("\n".join(cmts))
del cmts[:]
return
stmt = re.sub(r";\s*$", "", "\n".join(buf).strip())
head = ("\n".join(cmts) + "\n") if cmts else ""
out.append(head + stmt + delim)
del buf[:]
del cmts[:]
for raw in body.replace("\r\n", "\n").split("\n"):
line = raw.rstrip()
s = line.strip()
if not s:
continue
if c.startswith("--") or c.startswith("SET ") or c.startswith("GRANT") or c.startswith("REVOKE"):
out.append(c if c.endswith(";") else c + ";")
elif c.startswith("DROP PROCEDURE") or c.startswith("DROP TRIGGER") or c.startswith("DROP EVENT"):
out.append(c if c.endswith(";") else c + ";")
else:
stmt = c.rstrip(";")
out.append(stmt + "$$")
if not buf:
if s.startswith("--") or s.startswith("#") or not _STMT_HEAD.match(s):
cmts.append(line)
continue
if _BLOCK_OPEN.match(s):
depth[0] += 1
elif _BLOCK_CLOSE.match(s) and depth[0] > 0:
depth[0] -= 1
buf.append(line)
if depth[0] == 0 and s.endswith(";") and not _SUB_CLOSE.match(s):
flush()
flush()
return "\n\n".join(out) + "\n"
def _delimiter_integrity_errors(text, label=""):
"""DELIMITER 语句完整性静态断言QC #16PASS 要能证明「可执行」,而非只查关键字存在)。
断言项
1) `DELIMITER x` `DELIMITER ;` 严格配对文件结束时不残留未闭合块
2) 每个 `$$` 段首个代码行必须是**完整**语句头关键字 + 对象类型
禁止 `CREATE$$` / `DROP$$` 这类孤立残缺 token服务端 ERROR 1064
3) 一个 `$$` 段内不得再出现第二条 DDL 语句头防止 `;` 结尾语句与下一条黏连
4) 未以 `$$` 收尾的残段里若含可执行语句纯注释尾巴放行即判失败
返回缺陷描述列表空列表 = 结构通过
"""
errs = []
pfx = ("[%s] " % label) if label else ""
lines = text.split("\n")
delim = [None]
seg = [[]]
def code_of(ls):
return [x.strip() for x in ls if x.strip() and not x.strip().startswith("--")
and not x.strip().startswith("#")]
def close_segment():
code = code_of(seg[0])
if not code:
seg[0] = []
return
d = delim[0] or "$$"
first_words = code[0].replace(d, "").split()
if len(first_words) < 2:
errs.append(pfx + "残缺语句 token服务端按独立语句解析 → ERROR 1064: %r" % code[0])
for x in code[1:]:
if _INLINE_DDL_HEAD.match(x):
errs.append(pfx + "一个 %s 段内黏连了多条 DDL 语句: %r" % (d, x.strip()))
seg[0] = []
for i, ln in enumerate(lines, 1):
s = ln.strip()
if _BARE_TOKEN.match(s):
errs.append(pfx + "L%d 裸关键字$$ 残缺行: %r" % (i, s))
if s.upper().startswith("DELIMITER"):
parts = s.split(None, 1)
new = parts[1].strip() if len(parts) > 1 else ";"
if seg[0]:
close_segment()
if delim[0] is None:
if new == ";":
errs.append(pfx + "L%d 出现没有配对的 `DELIMITER ;`" % i)
else:
delim[0] = new
else:
if new != ";":
errs.append(pfx + "L%d DELIMITER %s 未闭合就切换到 %s"
% (i, delim[0], new))
delim[0] = new
else:
delim[0] = None
continue
if delim[0]:
seg[0].append(ln)
if s.endswith(delim[0]):
close_segment()
if seg[0]:
errs.append(pfx + "DELIMITER 块末尾存在未以 %s 收尾的可执行残段: %r"
% (delim[0] or "$$", code_of(seg[0])[0][:60]))
seg[0] = []
if delim[0] is not None:
errs.append(pfx + "文件结束时 DELIMITER %s 仍未闭合" % delim[0])
return errs
# ---------------------------------------------------------------- 迁移文件产出
MIGRATION_FILES = [
("20260919_m11b_01_create_pbl_runtime_event.sql",
@ -526,6 +643,43 @@ def verify():
msgs.append("FAIL DDL 非幂等(缺 IF EXISTS / DROP IF EXISTS")
else:
msgs.append("PASS 幂等IF NOT EXISTS + DROP ... IF EXISTS + 分区存在性判断")
# 8. DELIMITER 语句完整性QC #16PASS 必须能证明「可执行」,不能只查关键字存在性)
artifacts = [
("01", create_table_sql()),
("02", MIGRATION_FILES[1][1]("pbls")),
("03", MIGRATION_FILES[2][1]("pbls")),
("canonical", full_script()),
]
struct_errs = []
for label, text in artifacts:
struct_errs.extend(_delimiter_integrity_errors(text, label))
if struct_errs:
ok = False
for e in struct_errs:
msgs.append("FAIL DELIMITER 语句完整性: " + e)
else:
msgs.append("PASS DELIMITER 语句完整性: 4 个产物 DELIMITER 配对正确、%d 个 $$ 段首关键字完整、"
"无裸 CREATE$$/DROP$$ 残缺行"
% sum(len(re.findall(r"\$\$\s*$", t, re.M)) for _, t in artifacts))
# 9. 幂等/结构关键字逐产物覆盖QC #17完整陈述不再跳号回避缺陷
need_map = [
("01", ("CREATE TABLE IF NOT EXISTS", "PARTITION BY RANGE COLUMNS",
"VALUES LESS THAN (MAXVALUE)")),
("02", ("DROP PROCEDURE IF EXISTS", "DROP TRIGGER IF EXISTS",
"BEFORE UPDATE", "BEFORE DELETE")),
("03", ("DROP PROCEDURE IF EXISTS", "DROP EVENT IF EXISTS", "CREATE EVENT")),
]
by_label = dict(artifacts)
miss_all = []
for label, needs in need_map:
miss_all.extend("%s:%s" % (label, n) for n in needs if n not in by_label[label])
if miss_all:
ok = False
msgs.append("FAIL 幂等/结构关键字缺失: " + ", ".join(miss_all))
else:
msgs.append("PASS 幂等关键字逐文件覆盖: 01 建表分区 / 02 过程+双触发器 / 03 双过程+EVENT")
return ok, msgs
@ -632,16 +786,21 @@ def _norm_part(name):
return text.strip().strip("`").strip('"').strip("'").lower()
def _render(sql_tpl, args=(), placeholders=None):
"""渲染 SQL 模板里的**标识符**占位符(表名/分区名),并对 % 转义做 fail-fast 自检
def _render(sql, args=None, placeholders=None):
"""SQL 的 % 转义统一出口:可选渲染标识符 + fail-fast 自检QC #1 的根因处置)
约定QC #1 的根因处置):模板中由 Python 填入的标识符写 %s要交给 DBAPI 绑参的
百分号占位符一律写 %%s经本函数渲染后变回 %s渲染后
· 仍残留 '%%' 转义写错抛错
· '%s' 个数与预期绑参个数不符 抛错
约定模板中由 Python 侧填入的标识符表名/分区名 %s要留给 DBAPI 绑参的
百分号占位符一律写 %%s标识符渲染后即变回 %s
· args 不为 None先执行 sql % args 完成标识符渲染
· args None视为调用方已完成渲染模板就地 % 本函数只做转义自检
自检两条任一不满足立即抛 RuntimeError而不是丢给驱动报
"unsupported format character"那会在 verify_db 里被 except 吞成一条模糊失败
· 结果仍残留 '%%' 转义写错
· '%s' 个数与 placeholders 不符 漏写/多写绑参占位符
渲染单独成行括号显式定界不再依赖相邻字面量先拼接再整体 %的隐式优先级
"""
sql = sql_tpl % tuple(args)
if args is not None:
sql = sql % tuple(args)
if "%%" in sql:
raise RuntimeError("SQL 渲染后仍残留 '%%',转义写法有误: " + sql[:200])
if placeholders is not None and sql.count("%s") != placeholders:

View File

@ -60,9 +60,7 @@ DELIMITER $$
-- ---------------------------------------------------------------- B. append-only
-- append-only 断言过程:非白名单操作一律 SIGNAL 拒绝
DROP PROCEDURE IF EXISTS `pbl_assert_append_only`;
CREATE$$
DROP PROCEDURE IF EXISTS `pbl_assert_append_only`$$
CREATE PROCEDURE `pbl_assert_append_only`(
IN p_table VARCHAR(64), IN p_priv VARCHAR(16), IN p_detail VARCHAR(255))
@ -77,13 +75,11 @@ BEGIN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED',
MYSQL_ERRNO = 45001;
END;
END$$
-- BEFORE UPDATE 护栏:把违规语句原文带进错误消息,便于定位调用方
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_upd`$$
CREATE$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_upd`
BEFORE UPDATE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
@ -105,13 +101,11 @@ BEGIN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_UPDATE',
MYSQL_ERRNO = 45002;
END IF;
END;
END$$
-- BEFORE DELETE 护栏
DROP TRIGGER IF EXISTS `trg_pbl_runtime_event_append_only_del`$$
CREATE$$
CREATE TRIGGER `trg_pbl_runtime_event_append_only_del`
BEFORE DELETE ON `pbl_runtime_event` FOR EACH ROW
BEGIN
@ -133,17 +127,15 @@ BEGIN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PBL_APPEND_ONLY_DENIED_DELETE',
MYSQL_ERRNO = 45003;
END IF;
END;
END$$
-- 权限层加固(可选,需 DBA 账号执行app 账号只保留增/查)
-- GRANT INSERT, SELECT, UPDATE, DELETE ON `pbls`.`pbl_runtime_event` TO NULL; -- 占位,勿执行
-- REVOKE UPDATE, DELETE ON `pbls`.`pbl_runtime_event` FROM 'pbls'@'%'$$
-- REVOKE UPDATE, DELETE ON `pbls`.`pbl_runtime_event` FROM 'pbls'@'%';
-- ---------------------------------------------------------------- C. 分区维护
-- 1) 幂等补建未来月分区pmax 存在时用 REORGANIZE PARTITION 拆出新月份(不丢数据)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_ensure_partitions`;
CREATE$$
DROP PROCEDURE IF EXISTS `pbl_runtime_event_ensure_partitions`$$
CREATE PROCEDURE `pbl_runtime_event_ensure_partitions`(IN p_months_ahead INT)
MODIFIES SQL DATA
@ -185,13 +177,11 @@ BEGIN
END IF;
END LOOP;
CLOSE cur;
END;
END$$
-- 2) 到期归档:整月 DROP PARTITIONappend-only 表唯一合规的清理手段)
DROP PROCEDURE IF EXISTS `pbl_runtime_event_prune_partitions`$$
CREATE$$
CREATE PROCEDURE `pbl_runtime_event_prune_partitions`(IN p_keep_months INT)
MODIFIES SQL DATA
BEGIN
@ -218,16 +208,11 @@ BEGIN
DEALLOCATE PREPARE pbl_drop_st;
END LOOP;
CLOSE cur;
END;
END$$
-- 3) 每月自动补分区(需 event_scheduler=ON否则用 cron见开发说明
-- SET GLOBAL event_scheduler = ON$$
DROP$$
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`;
CREATE$$
-- SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS `ev_pbl_runtime_event_partition_maintain`$$
CREATE EVENT IF NOT EXISTS `ev_pbl_runtime_event_partition_maintain`
ON SCHEDULE EVERY 1 MONTH