From ba1cca9521648594aa7018023b22a4c9223bb9a5 Mon Sep 17 00:00:00 2001 From: Dawnforger Date: Sat, 9 May 2026 00:45:38 -0500 Subject: [PATCH] fix(db_world): clamp VerifiedBuild for MEDIUMINT (spell_target_position) MariaDB rejects 34149345 for VerifiedBuild when the column is MEDIUMINT (signed max 8388607), as in old 8.x schema migrations. Replace corrupt value with 0 in archive and base dumps. Add data/sql/tools/clamp_verifiedbuild_mediumint.py to re-scan archive, updates, and base INSERT/UPDATE blocks that list VerifiedBuild. Co-authored-by: Cursor --- data/sql/archive/db_world/2023_08_06_11.sql | 2 +- .../base/db_world/spell_target_position.sql | 2 +- .../tools/clamp_verifiedbuild_mediumint.py | 271 ++++++++++++++++++ 3 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 data/sql/tools/clamp_verifiedbuild_mediumint.py diff --git a/data/sql/archive/db_world/2023_08_06_11.sql b/data/sql/archive/db_world/2023_08_06_11.sql index 022b040..f0dfe49 100644 --- a/data/sql/archive/db_world/2023_08_06_11.sql +++ b/data/sql/archive/db_world/2023_08_06_11.sql @@ -3,7 +3,7 @@ UPDATE `creature_template` SET `scriptname` = '' WHERE `entry` = 27326; DELETE FROM `spell_target_position` WHERE `ID` = 48324 AND `EffectIndex` = 0; INSERT INTO `spell_target_position` (`ID`, `EffectIndex`, `MapID`, `PositionX`, `PositionY`, `PositionZ`, `Orientation`, `VerifiedBuild`) VALUES -(48324, 0, 571, 3454.11, -2802.37, 202.14, 0, 34149345); +(48324, 0, 571, 3454.11, -2802.37, 202.14, 0, 0); DELETE FROM `spell_script_names` WHERE `spell_id` IN (48382, 47533); INSERT INTO `spell_script_names` (`spell_id`, `ScriptName`) VALUES diff --git a/data/sql/base/db_world/spell_target_position.sql b/data/sql/base/db_world/spell_target_position.sql index d0ce98f..9f3c164 100644 --- a/data/sql/base/db_world/spell_target_position.sql +++ b/data/sql/base/db_world/spell_target_position.sql @@ -546,7 +546,7 @@ INSERT INTO `spell_target_position` VALUES (48274,0,575,302.36,-352.01,90.54,2.2,0), (48275,0,575,291.39,-352.01,90.54,0.91,0), (48276,0,575,296.651,-346.293,108.547,1.58,0), -(48324,0,571,3454.11,-2802.37,202.14,0,34149345), +(48324,0,571,3454.11,-2802.37,202.14,0,0), (48622,0,571,4274.53,-3055.55,319.463,2.535,0), (48760,0,571,3876.16,6984.44,106.32,6.279,0), (48960,0,571,4313.37,-2958.17,318.463,1.98,0), diff --git a/data/sql/tools/clamp_verifiedbuild_mediumint.py b/data/sql/tools/clamp_verifiedbuild_mediumint.py new file mode 100644 index 0000000..e43c1fc --- /dev/null +++ b/data/sql/tools/clamp_verifiedbuild_mediumint.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python3 +""" +Clamp VerifiedBuild literals to signed MEDIUMINT range for DBs that still use +MEDIUMINT on VerifiedBuild (see data/sql/old/db_world/8.x/2022_10_30_01.sql). + +Processes INSERT (explicit column lists) and simple UPDATE ... VerifiedBuild = N. +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + +MAX_SIGNED_MEDIUMINT = 8388607 +MIN_SIGNED_MEDIUMINT = -8388608 + +INSERT_RE = re.compile( + r"INSERT\s+INTO\s+`(?P\w+)`\s*\((?P[^)]+)\)\s*VALUES\s*", + re.IGNORECASE, +) + +# Base dumps: full row order ends with VerifiedBuild (6- or 8-column layouts). +INSERT_SPELL_TP_VALUES_RE = re.compile( + r"INSERT\s+INTO\s+`spell_target_position`\s*VALUES\s*", + re.IGNORECASE, +) + +STMT_START = ( + "INSERT ", + "DELETE ", + "UPDATE ", + "ALTER ", + "DROP ", + "CREATE ", + "SET ", + "REPLACE ", +) + + +def split_fields(inner: str) -> list[str]: + fields: list[str] = [] + cur: list[str] = [] + depth = 0 + in_str = False + str_ch = "" + esc = False + for ch in inner: + if in_str: + cur.append(ch) + if esc: + esc = False + elif ch == "\\": + esc = True + elif ch == str_ch: + in_str = False + continue + if ch in "'\"": + in_str = True + str_ch = ch + cur.append(ch) + continue + if ch == "(": + depth += 1 + elif ch == ")": + depth -= 1 + if ch == "," and depth == 0: + fields.append("".join(cur).strip()) + cur = [] + else: + cur.append(ch) + fields.append("".join(cur).strip()) + return fields + + +def join_fields(fields: list[str]) -> str: + return ", ".join(fields) + + +def clamp_tuple(tuple_str: str, vb_idx: int) -> str: + t = tuple_str.strip() + if not (t.startswith("(") and t.endswith(")")): + return tuple_str + fields = split_fields(t[1:-1]) + if vb_idx >= len(fields): + return tuple_str + raw = fields[vb_idx].strip() + if not re.fullmatch(r"-?\d+", raw): + return tuple_str + n = int(raw) + if MIN_SIGNED_MEDIUMINT <= n <= MAX_SIGNED_MEDIUMINT: + return tuple_str + fields[vb_idx] = "0" + return "(" + join_fields(fields) + ")" + + +def process_value_segment(segment: str, vb_idx: int) -> tuple[str, int]: + out: list[str] = [] + changes = 0 + i, n = 0, len(segment) + while i < n: + if segment[i] == "(": + depth = 1 + j = i + 1 + while j < n and depth > 0: + if segment[j] == "(": + depth += 1 + elif segment[j] == ")": + depth -= 1 + j += 1 + tup = segment[i:j] + new_tup = clamp_tuple(tup, vb_idx) + if new_tup != tup: + changes += 1 + out.append(new_tup) + i = j + continue + out.append(segment[i]) + i += 1 + return "".join(out), changes + + +def clamp_update_verifiedbuild(line: str) -> tuple[str, int]: + changes = 0 + + def repl(m: re.Match[str]) -> str: + nonlocal changes + num = m.group(1) + n = int(num) + if MIN_SIGNED_MEDIUMINT <= n <= MAX_SIGNED_MEDIUMINT: + return m.group(0) + changes += 1 + return f"`VerifiedBuild` = 0" + + new_line = re.sub( + r"`VerifiedBuild`\s*=\s*([0-9]+)", + repl, + line, + flags=re.IGNORECASE, + ) + return new_line, changes + + +def first_tuple_vb_index(segment: str) -> int | None: + i, n = 0, len(segment) + while i < n: + if segment[i] == "(": + depth = 1 + j = i + 1 + while j < n and depth > 0: + if segment[j] == "(": + depth += 1 + elif segment[j] == ")": + depth -= 1 + j += 1 + tup = segment[i:j] + fields = split_fields(tup[1:-1]) + if fields: + return len(fields) - 1 + return None + i += 1 + return None + + +def process_sql_text(text: str) -> tuple[str, int]: + lines = text.splitlines(keepends=True) + out: list[str] = [] + mode: str | None = None + vb_idx = 0 + total_changes = 0 + + for line in lines: + nl, ch = clamp_update_verifiedbuild(line) + total_changes += ch + line = nl + + stripped = line.lstrip() + + if mode == "insert_values": + if stripped and not stripped.startswith("(") and not stripped.startswith("--"): + if any(stripped.startswith(p) for p in STMT_START): + mode = None + + if mode == "spell_tp_values": + if stripped and not stripped.startswith("(") and not stripped.startswith("--"): + if any(stripped.startswith(p) for p in STMT_START): + mode = None + + if mode is None: + m = INSERT_RE.search(line) + if m and "`VerifiedBuild`" in m.group("cols"): + cols = [c.strip().strip("`") for c in m.group("cols").split(",")] + vb_idx = cols.index("VerifiedBuild") + mode = "insert_values" + prefix = line[: m.end()] + suffix = line[m.end() :] + if suffix.strip(): + new_suf, ch2 = process_value_segment(suffix, vb_idx) + total_changes += ch2 + line = prefix + new_suf + if ");" in line or line.rstrip().endswith(");"): + mode = None + out.append(line) + continue + m2 = INSERT_SPELL_TP_VALUES_RE.search(line) + if m2: + mode = "spell_tp_values" + vb_idx = -1 + prefix = line[: m2.end()] + suffix = line[m2.end() :] + if suffix.strip(): + vb_idx = first_tuple_vb_index(suffix) + if vb_idx is None: + vb_idx = 7 + new_suf, ch2 = process_value_segment(suffix, vb_idx) + total_changes += ch2 + line = prefix + new_suf + if ");" in line or line.rstrip().endswith(");"): + mode = None + out.append(line) + continue + out.append(line) + continue + + if mode == "spell_tp_values": + if vb_idx < 0: + vb_idx = first_tuple_vb_index(line) + if vb_idx is None: + vb_idx = 7 + new_line, ch3 = process_value_segment(line, vb_idx) + total_changes += ch3 + out.append(new_line) + if ");" in line or line.rstrip().endswith(");"): + mode = None + continue + + # insert_values continuation + new_line, ch3 = process_value_segment(line, vb_idx) + total_changes += ch3 + out.append(new_line) + if ");" in line or line.rstrip().endswith(");"): + mode = None + + return "".join(out), total_changes + + +def main() -> int: + sql_root = Path(__file__).resolve().parents[1] + roots = [ + sql_root / "archive" / "db_world", + sql_root / "updates" / "db_world", + sql_root / "base" / "db_world", + ] + file_count = 0 + change_lines = 0 + for root in roots: + if not root.exists(): + continue + for path in sorted(root.glob("*.sql")): + text = path.read_text(encoding="utf-8", errors="strict") + new_text, ch = process_sql_text(text) + if ch: + path.write_text(new_text, encoding="utf-8") + change_lines += ch + file_count += 1 + print(f"{path.relative_to(sql_root)}: {ch} change(s)") + print(f"Done. {file_count} file(s) written, {change_lines} change(s).") + return 0 + + +if __name__ == "__main__": + sys.exit(main())