Commit af9b6253b12c5381378412319cf423ffa6d42530
1 parent
abee9ff4
fix(sql): 修复 convertor 索引解析兼容问题
- 兼容 simple-ddl-parser 不同版本的索引列返回结构 - 修复唯一索引列中误混入 ASC/DESC 导致生成 SQL 不合法的问题 - 兼容 Oracle、Kingbase 的 level 等保留字列名转义 - 同步处理 CREATE、COMMENT、INSERT 显式列清单的列名转义
Showing
1 changed file
with
90 additions
and
17 deletions
sql/tools/convertor.py
| ... | ... | @@ -77,6 +77,9 @@ def load_and_clean(sql_file: str) -> str: |
| 77 | 77 | |
| 78 | 78 | |
| 79 | 79 | class Convertor(ABC): |
| 80 | + # 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。 | |
| 81 | + reserved_column_names = set() | |
| 82 | + | |
| 80 | 83 | def __init__(self, src: str, db_type) -> None: |
| 81 | 84 | self.src = src |
| 82 | 85 | self.db_type = db_type |
| ... | ... | @@ -179,6 +182,31 @@ class Convertor(ABC): |
| 179 | 182 | """ |
| 180 | 183 | return "" |
| 181 | 184 | |
| 185 | + def escape_column_name(self, name: str) -> str: | |
| 186 | + """转义目标库保留字列名,例如 Oracle / Kingbase 的 level。""" | |
| 187 | + | |
| 188 | + column_name = name.lower() | |
| 189 | + if column_name in self.reserved_column_names: | |
| 190 | + return f'"{column_name}"' | |
| 191 | + return column_name | |
| 192 | + | |
| 193 | + def escape_insert_columns(self, insert_script: str) -> str: | |
| 194 | + """INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。""" | |
| 195 | + | |
| 196 | + match = re.match( | |
| 197 | + r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)", | |
| 198 | + insert_script, | |
| 199 | + flags=re.IGNORECASE, | |
| 200 | + ) | |
| 201 | + if not match: | |
| 202 | + return insert_script | |
| 203 | + | |
| 204 | + columns = [ | |
| 205 | + self.escape_column_name(column.strip()) | |
| 206 | + for column in match.group(2).split(",") | |
| 207 | + ] | |
| 208 | + return f"{match.group(1)}{', '.join(columns)}{match.group(3)}" | |
| 209 | + | |
| 182 | 210 | @staticmethod |
| 183 | 211 | def inserts(table_name: str, script_content: str) -> Generator: |
| 184 | 212 | PREFIX = f"INSERT INTO `{table_name}`" |
| ... | ... | @@ -204,26 +232,65 @@ class Convertor(ABC): |
| 204 | 232 | Generator[str]: create index 语句 |
| 205 | 233 | """ |
| 206 | 234 | |
| 207 | - def generate_columns(columns): | |
| 208 | - keys = [ | |
| 209 | - f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}" | |
| 210 | - for col in columns[0] | |
| 211 | - ] | |
| 212 | - return ", ".join(keys) | |
| 213 | - | |
| 214 | - for no, index in enumerate(ddl["index"], 1): | |
| 215 | - columns = generate_columns(index["columns"]) | |
| 235 | + for no, index in enumerate(ddl.get("index", []), 1): | |
| 236 | + columns = ", ".join(Convertor.index_columns(index.get("columns", []))) | |
| 237 | + if not columns: | |
| 238 | + continue | |
| 216 | 239 | table_name = ddl["table_name"].lower() |
| 217 | 240 | yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" |
| 218 | 241 | |
| 219 | 242 | @staticmethod |
| 243 | + def index_columns(columns) -> list: | |
| 244 | + """兼容 simple-ddl-parser 不同版本的索引列结构。""" | |
| 245 | + | |
| 246 | + keys = [] | |
| 247 | + | |
| 248 | + def append(name, order="ASC"): | |
| 249 | + if not name: | |
| 250 | + return | |
| 251 | + column_name = str(name).strip("`").lower() | |
| 252 | + column_order = str(order or "ASC").upper() | |
| 253 | + if column_order == "DESC": | |
| 254 | + keys.append(f"{column_name} desc") | |
| 255 | + else: | |
| 256 | + keys.append(column_name) | |
| 257 | + | |
| 258 | + def visit(value): | |
| 259 | + # 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]] | |
| 260 | + if isinstance(value, (list, tuple)): | |
| 261 | + for item in value: | |
| 262 | + visit(item) | |
| 263 | + return | |
| 264 | + if isinstance(value, dict): | |
| 265 | + name = value.get("name") | |
| 266 | + if isinstance(name, (dict, list, tuple)): | |
| 267 | + visit(name) | |
| 268 | + return | |
| 269 | + append(name, value.get("order", "ASC")) | |
| 270 | + return | |
| 271 | + # 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。 | |
| 272 | + if isinstance(value, str): | |
| 273 | + token = value.strip("`") | |
| 274 | + order = token.upper() | |
| 275 | + if order in ("ASC", "DESC"): | |
| 276 | + if order == "DESC" and keys and not keys[-1].endswith(" desc"): | |
| 277 | + keys[-1] = f"{keys[-1]} desc" | |
| 278 | + return | |
| 279 | + append(token) | |
| 280 | + | |
| 281 | + visit(columns) | |
| 282 | + return keys | |
| 283 | + | |
| 284 | + @staticmethod | |
| 220 | 285 | def unique_index(ddl: Dict) -> Generator: |
| 221 | 286 | if "constraints" in ddl and "uniques" in ddl["constraints"]: |
| 222 | 287 | uk_list = ddl["constraints"]["uniques"] |
| 223 | 288 | for uk in uk_list: |
| 224 | 289 | table_name = ddl["table_name"] |
| 225 | 290 | uk_name = uk["constraint_name"] |
| 226 | - uk_columns = uk["columns"] | |
| 291 | + uk_columns = Convertor.index_columns(uk["columns"]) | |
| 292 | + if not uk_columns: | |
| 293 | + continue | |
| 227 | 294 | yield table_name, uk_name, uk_columns |
| 228 | 295 | |
| 229 | 296 | @staticmethod |
| ... | ... | @@ -381,7 +448,7 @@ class PostgreSQLConvertor(Convertor): |
| 381 | 448 | ) |
| 382 | 449 | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| 383 | 450 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 384 | - return f"{name} {full_type} {nullable} {default}" | |
| 451 | + return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" | |
| 385 | 452 | |
| 386 | 453 | table_name = ddl["table_name"].lower() |
| 387 | 454 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| ... | ... | @@ -406,7 +473,7 @@ CREATE TABLE {table_name} ( |
| 406 | 473 | for column in table_ddl["columns"]: |
| 407 | 474 | table_comment = column["comment"] |
| 408 | 475 | script += ( |
| 409 | - f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" | |
| 476 | + f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" | |
| 410 | 477 | + "\n" |
| 411 | 478 | ) |
| 412 | 479 | |
| ... | ... | @@ -435,6 +502,7 @@ CREATE TABLE {table_name} ( |
| 435 | 502 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| 436 | 503 | |
| 437 | 504 | inserts = list(Convertor.inserts(table_name, self.content)) |
| 505 | + inserts = [self.escape_insert_columns(s) for s in inserts] | |
| 438 | 506 | # 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> '' |
| 439 | 507 | inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts] |
| 440 | 508 | ## 生成 insert 脚本 |
| ... | ... | @@ -482,6 +550,8 @@ INSERT INTO dual VALUES (1); |
| 482 | 550 | |
| 483 | 551 | |
| 484 | 552 | class OracleConvertor(Convertor): |
| 553 | + reserved_column_names = {"level", "size"} | |
| 554 | + | |
| 485 | 555 | def __init__(self, src): |
| 486 | 556 | super().__init__(src, "Oracle") |
| 487 | 557 | |
| ... | ... | @@ -526,10 +596,8 @@ class OracleConvertor(Convertor): |
| 526 | 596 | # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL |
| 527 | 597 | nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable |
| 528 | 598 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 529 | - # Oracle 中 size 不能作为字段名 | |
| 530 | - field_name = '"size"' if name == "size" else name | |
| 531 | 599 | # Oracle DEFAULT 定义在 NULLABLE 之前 |
| 532 | - return f"{field_name} {full_type} {default} {nullable}" | |
| 600 | + return f"{self.escape_column_name(name)} {full_type} {default} {nullable}" | |
| 533 | 601 | |
| 534 | 602 | table_name = ddl["table_name"].lower() |
| 535 | 603 | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] |
| ... | ... | @@ -554,7 +622,7 @@ CREATE TABLE {table_name} ( |
| 554 | 622 | for column in table_ddl["columns"]: |
| 555 | 623 | table_comment = column["comment"] |
| 556 | 624 | script += ( |
| 557 | - f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" | |
| 625 | + f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" | |
| 558 | 626 | + "\n" |
| 559 | 627 | ) |
| 560 | 628 | |
| ... | ... | @@ -586,6 +654,7 @@ CREATE TABLE {table_name} ( |
| 586 | 654 | """拷贝 INSERT 语句""" |
| 587 | 655 | inserts = [] |
| 588 | 656 | for insert_script in Convertor.inserts(table_name, self.content): |
| 657 | + insert_script = self.escape_insert_columns(insert_script) | |
| 589 | 658 | # 对日期数据添加 TO_DATE 转换 |
| 590 | 659 | insert_script = re.sub( |
| 591 | 660 | r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", |
| ... | ... | @@ -907,6 +976,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF; |
| 907 | 976 | |
| 908 | 977 | |
| 909 | 978 | class KingbaseConvertor(PostgreSQLConvertor): |
| 979 | + reserved_column_names = {"level"} | |
| 980 | + | |
| 910 | 981 | def __init__(self, src): |
| 911 | 982 | super().__init__(src) |
| 912 | 983 | self.db_type = "Kingbase" |
| ... | ... | @@ -925,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor): |
| 925 | 996 | if full_type == "text": |
| 926 | 997 | nullable = "NULL" |
| 927 | 998 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 928 | - return f"{name} {full_type} {nullable} {default}" | |
| 999 | + return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" | |
| 929 | 1000 | |
| 930 | 1001 | table_name = ddl["table_name"].lower() |
| 931 | 1002 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| ... | ... | @@ -945,6 +1016,8 @@ CREATE TABLE {table_name} ( |
| 945 | 1016 | |
| 946 | 1017 | |
| 947 | 1018 | class OpengaussConvertor(KingbaseConvertor): |
| 1019 | + reserved_column_names = set() | |
| 1020 | + | |
| 948 | 1021 | def __init__(self, src): |
| 949 | 1022 | super().__init__(src) |
| 950 | 1023 | self.db_type = "OpenGauss" | ... | ... |