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,6 +77,9 @@ def load_and_clean(sql_file: str) -> str: | ||
| 77 | 77 | ||
| 78 | 78 | ||
| 79 | class Convertor(ABC): | 79 | class Convertor(ABC): |
| 80 | + # 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。 | ||
| 81 | + reserved_column_names = set() | ||
| 82 | + | ||
| 80 | def __init__(self, src: str, db_type) -> None: | 83 | def __init__(self, src: str, db_type) -> None: |
| 81 | self.src = src | 84 | self.src = src |
| 82 | self.db_type = db_type | 85 | self.db_type = db_type |
| @@ -179,6 +182,31 @@ class Convertor(ABC): | @@ -179,6 +182,31 @@ class Convertor(ABC): | ||
| 179 | """ | 182 | """ |
| 180 | return "" | 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 | @staticmethod | 210 | @staticmethod |
| 183 | def inserts(table_name: str, script_content: str) -> Generator: | 211 | def inserts(table_name: str, script_content: str) -> Generator: |
| 184 | PREFIX = f"INSERT INTO `{table_name}`" | 212 | PREFIX = f"INSERT INTO `{table_name}`" |
| @@ -204,26 +232,65 @@ class Convertor(ABC): | @@ -204,26 +232,65 @@ class Convertor(ABC): | ||
| 204 | Generator[str]: create index 语句 | 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 | table_name = ddl["table_name"].lower() | 239 | table_name = ddl["table_name"].lower() |
| 217 | yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" | 240 | yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" |
| 218 | 241 | ||
| 219 | @staticmethod | 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 | def unique_index(ddl: Dict) -> Generator: | 285 | def unique_index(ddl: Dict) -> Generator: |
| 221 | if "constraints" in ddl and "uniques" in ddl["constraints"]: | 286 | if "constraints" in ddl and "uniques" in ddl["constraints"]: |
| 222 | uk_list = ddl["constraints"]["uniques"] | 287 | uk_list = ddl["constraints"]["uniques"] |
| 223 | for uk in uk_list: | 288 | for uk in uk_list: |
| 224 | table_name = ddl["table_name"] | 289 | table_name = ddl["table_name"] |
| 225 | uk_name = uk["constraint_name"] | 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 | yield table_name, uk_name, uk_columns | 294 | yield table_name, uk_name, uk_columns |
| 228 | 295 | ||
| 229 | @staticmethod | 296 | @staticmethod |
| @@ -381,7 +448,7 @@ class PostgreSQLConvertor(Convertor): | @@ -381,7 +448,7 @@ class PostgreSQLConvertor(Convertor): | ||
| 381 | ) | 448 | ) |
| 382 | nullable = "NULL" if col["nullable"] else "NOT NULL" | 449 | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| 383 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" | 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 | table_name = ddl["table_name"].lower() | 453 | table_name = ddl["table_name"].lower() |
| 387 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] | 454 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| @@ -406,7 +473,7 @@ CREATE TABLE {table_name} ( | @@ -406,7 +473,7 @@ CREATE TABLE {table_name} ( | ||
| 406 | for column in table_ddl["columns"]: | 473 | for column in table_ddl["columns"]: |
| 407 | table_comment = column["comment"] | 474 | table_comment = column["comment"] |
| 408 | script += ( | 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 | + "\n" | 477 | + "\n" |
| 411 | ) | 478 | ) |
| 412 | 479 | ||
| @@ -435,6 +502,7 @@ CREATE TABLE {table_name} ( | @@ -435,6 +502,7 @@ CREATE TABLE {table_name} ( | ||
| 435 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" | 502 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| 436 | 503 | ||
| 437 | inserts = list(Convertor.inserts(table_name, self.content)) | 504 | inserts = list(Convertor.inserts(table_name, self.content)) |
| 505 | + inserts = [self.escape_insert_columns(s) for s in inserts] | ||
| 438 | # 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> '' | 506 | # 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> '' |
| 439 | inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts] | 507 | inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts] |
| 440 | ## 生成 insert 脚本 | 508 | ## 生成 insert 脚本 |
| @@ -482,6 +550,8 @@ INSERT INTO dual VALUES (1); | @@ -482,6 +550,8 @@ INSERT INTO dual VALUES (1); | ||
| 482 | 550 | ||
| 483 | 551 | ||
| 484 | class OracleConvertor(Convertor): | 552 | class OracleConvertor(Convertor): |
| 553 | + reserved_column_names = {"level", "size"} | ||
| 554 | + | ||
| 485 | def __init__(self, src): | 555 | def __init__(self, src): |
| 486 | super().__init__(src, "Oracle") | 556 | super().__init__(src, "Oracle") |
| 487 | 557 | ||
| @@ -526,10 +596,8 @@ class OracleConvertor(Convertor): | @@ -526,10 +596,8 @@ class OracleConvertor(Convertor): | ||
| 526 | # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL | 596 | # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL |
| 527 | nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable | 597 | nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable |
| 528 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" | 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 | # Oracle DEFAULT 定义在 NULLABLE 之前 | 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 | table_name = ddl["table_name"].lower() | 602 | table_name = ddl["table_name"].lower() |
| 535 | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] | 603 | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] |
| @@ -554,7 +622,7 @@ CREATE TABLE {table_name} ( | @@ -554,7 +622,7 @@ CREATE TABLE {table_name} ( | ||
| 554 | for column in table_ddl["columns"]: | 622 | for column in table_ddl["columns"]: |
| 555 | table_comment = column["comment"] | 623 | table_comment = column["comment"] |
| 556 | script += ( | 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 | + "\n" | 626 | + "\n" |
| 559 | ) | 627 | ) |
| 560 | 628 | ||
| @@ -586,6 +654,7 @@ CREATE TABLE {table_name} ( | @@ -586,6 +654,7 @@ CREATE TABLE {table_name} ( | ||
| 586 | """拷贝 INSERT 语句""" | 654 | """拷贝 INSERT 语句""" |
| 587 | inserts = [] | 655 | inserts = [] |
| 588 | for insert_script in Convertor.inserts(table_name, self.content): | 656 | for insert_script in Convertor.inserts(table_name, self.content): |
| 657 | + insert_script = self.escape_insert_columns(insert_script) | ||
| 589 | # 对日期数据添加 TO_DATE 转换 | 658 | # 对日期数据添加 TO_DATE 转换 |
| 590 | insert_script = re.sub( | 659 | insert_script = re.sub( |
| 591 | r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", | 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,6 +976,8 @@ SET IDENTITY_INSERT {table_name.lower()} OFF; | ||
| 907 | 976 | ||
| 908 | 977 | ||
| 909 | class KingbaseConvertor(PostgreSQLConvertor): | 978 | class KingbaseConvertor(PostgreSQLConvertor): |
| 979 | + reserved_column_names = {"level"} | ||
| 980 | + | ||
| 910 | def __init__(self, src): | 981 | def __init__(self, src): |
| 911 | super().__init__(src) | 982 | super().__init__(src) |
| 912 | self.db_type = "Kingbase" | 983 | self.db_type = "Kingbase" |
| @@ -925,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor): | @@ -925,7 +996,7 @@ class KingbaseConvertor(PostgreSQLConvertor): | ||
| 925 | if full_type == "text": | 996 | if full_type == "text": |
| 926 | nullable = "NULL" | 997 | nullable = "NULL" |
| 927 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" | 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 | table_name = ddl["table_name"].lower() | 1001 | table_name = ddl["table_name"].lower() |
| 931 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] | 1002 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| @@ -945,6 +1016,8 @@ CREATE TABLE {table_name} ( | @@ -945,6 +1016,8 @@ CREATE TABLE {table_name} ( | ||
| 945 | 1016 | ||
| 946 | 1017 | ||
| 947 | class OpengaussConvertor(KingbaseConvertor): | 1018 | class OpengaussConvertor(KingbaseConvertor): |
| 1019 | + reserved_column_names = set() | ||
| 1020 | + | ||
| 948 | def __init__(self, src): | 1021 | def __init__(self, src): |
| 949 | super().__init__(src) | 1022 | super().__init__(src) |
| 950 | self.db_type = "OpenGauss" | 1023 | self.db_type = "OpenGauss" |