#!/usr/bin/env python3 # -*- coding: utf-8 -*- # M11b-1a 定点补丁脚本(一次性工具,跑完即删): # Patch1 maintenance_sql(): 百分号格式化渲染 → token + str.replace,避开 % 格式化崩溃(QC #1) # Patch2 verify_db(): 删空转查询;落分区/分区裁剪改用 information_schema 分区清单 # + SELECT ... PARTITION(p) 反查 + EXPLAIN FORMAT=JSON 解析 partitions(QC #6) # Patch3 新增连库辅助函数 _explain_partitions/_row_in_partition/_locate_row_partition # DDL 列定义/索引/分区计划文本一律不动。 import os HERE = os.path.dirname(os.path.abspath(__file__)) P = os.path.join(os.path.dirname(HERE), "scripts", "pbl_runtime_event_ddl.py") src = open(P, encoding="utf-8").read() orig = src # ================================================================ Patch 1 i0 = src.index("def maintenance_sql(") i1 = src.index("def migrate_from_legacy_sql()") region = src[i0:i1] assert "%(table)s" in region and "%(pmax)s" in region and "%(months)s" in region, "占位符缺失" new_region = (region .replace("%(table)s", "__TABLE__") .replace("%(pmax)s", "__PMAX__") .replace("%(months)s", "__MONTHS__")) old_doc = ' """分区维护:过程(幂等补建月分区 / 到期 DROP 旧分区)+ 每月 EVENT。"""' new_doc = (' """分区维护:过程(幂等补建月分区 / 到期 DROP 旧分区)+ 每月 EVENT。\n' '\n' " QC #1 修复:正文含大量字面百分号(DATE_FORMAT(...,'%Y-%m-01')、'%Y%m'),\n" ' 原先用「三引号 % 字典」渲染会抛 ValueError: unsupported format character。\n' ' 现改用 __TABLE__/__PMAX__/__MONTHS__ token + str.replace,\n' ' 字面百分号原样输出、无需再写双写转义。\n' ' """') assert old_doc in new_region, "docstring 未命中" new_region = new_region.replace(old_doc, new_doc) old_tail = ('""" % {"table": TABLE, "pmax": PARTITION_MAXVALUE, "months": months,\n' ' "today": (today or date.today()).isoformat()}') new_tail = ('""".replace("__TABLE__", TABLE)\n' ' .replace("__PMAX__", PARTITION_MAXVALUE)\n' ' .replace("__MONTHS__", str(months))') assert old_tail in new_region, "maintenance_sql 渲染尾段未命中" new_region = new_region.replace(old_tail, new_tail) src = src[:i0] + new_region + src[i1:] # ================================================================ Patch 2 s2 = src.index(' cur.execute("SELECT PARTITION_NAME FROM information_schema.PARTITIONS "') e2_marker = (' results.append(("分区裁剪", pruned.count(",") == 0 and "p" in pruned,' ' "EXPLAIN partitions=%s" % pruned))') e2 = src.index(e2_marker) + len(e2_marker) old_block = src[s2:e2] assert "EXPLAIN PARTITIONS SELECT id FROM `%s` WHERE event_uid" in old_block, "旧判定块未命中" assert "for k in" in old_block, "空转 for 循环不在预期块内" new_block = ''' # 分区清单:以 information_schema.PARTITIONS 为权威来源(按 ordinal 排序), # 后面「落哪个分区」「分区裁剪」两项判定都基于这份清单(QC #6) cur.execute("SELECT PARTITION_NAME, PARTITION_DESCRIPTION FROM information_schema.PARTITIONS " "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=%s " "AND PARTITION_NAME IS NOT NULL ORDER BY PARTITION_ORDINAL_POSITION", (TABLE,)) part_rows = [(r[0], r[1]) for r in cur.fetchall()] part_names = [n for n, _ in part_rows if n] results.append(("MAXVALUE 兜底", PARTITION_MAXVALUE in part_names, ",".join(part_names))) # 插入两条不同月份的探针行 → 应落在两个不同的物理分区 probe = "m11bprobe%s" % date.today().strftime("%H%M%S") probes = (("%s_a" % probe, "2026-01-05 00:00:00"), ("%s_b" % probe, "2027-03-05 00:00:00")) try: for i, (uid, ts) in enumerate(probes): cur.execute("INSERT INTO `%s` (tenant_id,event_uid,event_type,scene_id,seq,created_at)" " VALUES ('t_probe',%%s,'probe',0,%%s,%%s)" % TABLE, (uid, i + 1, ts)) except Exception as exc: # noqa: BLE001 results.append(("跨月插入", False, str(exc)[:160])) else: results.append(("跨月插入", True, "%d 条探针行(不同月份)" % len(probes))) located = [_locate_row_partition(cur, uid, ts, part_names) for uid, ts in probes] distinct = len(set(located)) == 2 in_list = all(p in part_names for p in located) results.append(("跨月落不同分区", distinct and in_list, " | ".join("%s -> %s" % (uid, p) for (uid, _), p in zip(probes, located)))) # 分区裁剪:created_at 区间只应命中该区间所属的少数分区,而非全部分区。 # 旧写法 EXPLAIN PARTITIONS 取 row[3]:列序跨版本不稳定,且该语法新版 MySQL 已废弃。 pruned = _explain_partitions(cur, "SELECT id FROM `%s` WHERE created_at>='2026-01-01'" " AND created_at<'2026-02-01'" % TABLE) results.append(("分区裁剪", bool(pruned) and all(p in part_names for p in pruned) and len(pruned) < max(1, len(part_names)), "命中 %d/%d 分区: %s" % (len(pruned), len(part_names), ",".join(pruned) or "N/A")))''' src = src[:s2] + new_block + src[e2:] # ================================================================ Patch 3 helpers = '''def _explain_partitions(cur, sql, params=None): """EXPLAIN FORMAT=JSON 取执行计划实际扫描的分区清单(QC #6)。 不再用 EXPLAIN PARTITIONS:其分区列固定在结果 row[3],列序随 MariaDB/MySQL 版本变化,且该扩展语法在新版 MySQL 已废弃。JSON 计划里的 partitions 字段两版一致。 取不到(引擎不支持/解析失败)返回 [],由调用方判失败,不猜。 """ try: cur.execute("EXPLAIN FORMAT=JSON " + sql, tuple(params or ())) row = cur.fetchone() except Exception: # noqa: BLE001 return [] raw = row[0] if row else None if isinstance(raw, (bytes, bytearray)): raw = raw.decode("utf-8", "replace") if not raw: return [] try: plan = json.loads(raw) except (ValueError, TypeError): return [] found = [] def walk(node): if isinstance(node, dict): for key, val in node.items(): if key == "partitions" and isinstance(val, list): for p in val: if isinstance(p, str) and p and p not in found: found.append(p) else: walk(val) elif isinstance(node, list): for val in node: walk(val) walk(plan) return found def _row_in_partition(cur, part, uid): """SELECT ... FROM t PARTITION(p) 反查:探针行是否物理落在该分区。""" try: cur.execute("SELECT id FROM `%s` PARTITION (`%s`)" " WHERE tenant_id='t_probe' AND event_uid=%%s" % (TABLE, part), (uid,)) except Exception: # noqa: BLE001 return False return bool(cur.fetchone()) def _locate_row_partition(cur, uid, ts, part_names): """定位某探针行实际所在分区(QC #6)。 1) 首选按 information_schema 分区清单逐个 PARTITION(p) 反查物理位置——与 EXPLAIN 输出列序无关,最可靠; 2) 反查不中(引擎不支持 PARTITION 子句等)时退回 EXPLAIN FORMAT=JSON 解析 partitions(带 tenant_id+event_uid+created_at 等值条件,裁剪后应只剩 1 个分区)。 """ for part in part_names: if _row_in_partition(cur, part, uid): return part planned = _explain_partitions(cur, "SELECT id FROM `%s` WHERE tenant_id='t_probe'" " AND event_uid=%%s AND created_at=%%s" % TABLE, (uid, ts)) if len(planned) == 1: return planned[0] return ",".join(planned) or "?" ''' marker = "def verify_db():" assert src.count(marker) == 1, "verify_db 定位异常" src = src.replace(marker, helpers + marker) open(P, "w", encoding="utf-8").write(src) print("PATCHED %s: %d -> %d bytes" % (P, len(orig), len(src))) print("residual 'EXPLAIN PARTITIONS ' count:", src.count("EXPLAIN PARTITIONS ")) print("residual 'for k in' count:", src.count("for k in"))