Commit 4401c703ef59dc68d97530172d4845835eb9de43
1 parent
3140ec89
feat: 增加 UNIQUE KEY 支持
Showing
1 changed file
with
142 additions
and
44 deletions
sql/tools/convertor.py
| ... | ... | @@ -4,6 +4,14 @@ |
| 4 | 4 | Author: dhb52 (https://gitee.com/dhb52) |
| 5 | 5 | |
| 6 | 6 | pip install simple-ddl-parser |
| 7 | + | |
| 8 | +or with uv | |
| 9 | +uv run --with simple-ddl-parser convertor.py postgres > ../postgresql/ruoyi-vue-pro.sql 239ms 四 5/22 21:03:16 2025 | |
| 10 | +uv run --with simple-ddl-parser convertor.py sqlserver > ../sqlserver/ruoyi-vue-pro.sql | |
| 11 | +uv run --with simple-ddl-parser convertor.py kingbase > ../kingbase/ruoyi-vue-pro.sql | |
| 12 | +uv run --with simple-ddl-parser convertor.py opengauss > ../opengauss/ruoyi-vue-pro.sql | |
| 13 | +uv run --with simple-ddl-parser convertor.py oracle > ../oracle/ruoyi-vue-pro.sql | |
| 14 | +uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql | |
| 7 | 15 | """ |
| 8 | 16 | |
| 9 | 17 | import argparse |
| ... | ... | @@ -38,6 +46,7 @@ def load_and_clean(sql_file: str) -> str: |
| 38 | 46 | str: 清理后的sql文件内容 |
| 39 | 47 | """ |
| 40 | 48 | REPLACE_PAIR_LIST = ( |
| 49 | + (")\nVALUES ", ") VALUES "), | |
| 41 | 50 | (" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ", " "), |
| 42 | 51 | (" KEY `", " INDEX `"), |
| 43 | 52 | ("UNIQUE INDEX", "UNIQUE KEY"), |
| ... | ... | @@ -45,7 +54,7 @@ def load_and_clean(sql_file: str) -> str: |
| 45 | 54 | ("b'1'", "'1'"), |
| 46 | 55 | ) |
| 47 | 56 | |
| 48 | - content = open(sql_file).read() | |
| 57 | + content = open(sql_file, encoding="utf-8").read() | |
| 49 | 58 | for replace_pair in REPLACE_PAIR_LIST: |
| 50 | 59 | content = content.replace(*replace_pair) |
| 51 | 60 | content = re.sub(r"ENGINE.*COMMENT", "COMMENT", content) |
| ... | ... | @@ -110,12 +119,11 @@ class Convertor(ABC): |
| 110 | 119 | pass |
| 111 | 120 | |
| 112 | 121 | @abstractmethod |
| 113 | - def gen_comment(self, table_sql: str, table_name: str) -> str: | |
| 122 | + def gen_comment(self, table_ddl: Dict) -> str: | |
| 114 | 123 | """生成字段/表注释 |
| 115 | 124 | |
| 116 | 125 | Args: |
| 117 | - table_sql (str): 原始表SQL | |
| 118 | - table_name (str): 表名 | |
| 126 | + table_ddl (Dict): 表DDL | |
| 119 | 127 | |
| 120 | 128 | Returns: |
| 121 | 129 | str: 生成脚本 |
| ... | ... | @@ -123,6 +131,17 @@ class Convertor(ABC): |
| 123 | 131 | pass |
| 124 | 132 | |
| 125 | 133 | @abstractmethod |
| 134 | + def gen_uk(self, table_ddl: Dict) -> str: | |
| 135 | + """生成 | |
| 136 | + | |
| 137 | + Args: | |
| 138 | + table_ddl (Dict): 表DDL | |
| 139 | + | |
| 140 | + Returns: | |
| 141 | + str: 生成脚本 | |
| 142 | + """ | |
| 143 | + | |
| 144 | + @abstractmethod | |
| 126 | 145 | def gen_insert(self, table_name: str) -> str: |
| 127 | 146 | """生成 insert 语句块 |
| 128 | 147 | |
| ... | ... | @@ -179,6 +198,16 @@ class Convertor(ABC): |
| 179 | 198 | yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" |
| 180 | 199 | |
| 181 | 200 | @staticmethod |
| 201 | + def unique_index(ddl: Dict) -> Generator: | |
| 202 | + if "constraints" in ddl and "uniques" in ddl["constraints"]: | |
| 203 | + uk_list = ddl["constraints"]["uniques"] | |
| 204 | + for uk in uk_list: | |
| 205 | + table_name = ddl["table_name"] | |
| 206 | + uk_name = uk["constraint_name"] | |
| 207 | + uk_columns = uk["columns"] | |
| 208 | + yield table_name, uk_name, uk_columns | |
| 209 | + | |
| 210 | + @staticmethod | |
| 182 | 211 | def filed_comments(table_sql: str) -> Generator: |
| 183 | 212 | for line in table_sql.split("\n"): |
| 184 | 213 | match = re.match(r"^`([^`]+)`.* COMMENT '([^']+)'", line.strip()) |
| ... | ... | @@ -188,7 +217,7 @@ class Convertor(ABC): |
| 188 | 217 | yield field, comment_string |
| 189 | 218 | |
| 190 | 219 | def table_comment(self, table_sql: str) -> str: |
| 191 | - match = re.search(r"COMMENT \= '([^']+)';", table_sql) | |
| 220 | + match = re.search(r"COMMENT \='([^']+)';", table_sql) | |
| 192 | 221 | return match.group(1) if match else None |
| 193 | 222 | |
| 194 | 223 | def print(self): |
| ... | ... | @@ -226,11 +255,21 @@ class Convertor(ABC): |
| 226 | 255 | if table_name.lower().startswith("qrtz"): |
| 227 | 256 | continue |
| 228 | 257 | |
| 229 | - # 为每个表生成个5个基本部分 | |
| 258 | + # 解析注释 | |
| 259 | + for column in table_ddl["columns"]: | |
| 260 | + column["comment"] = bytes(column["comment"], "utf-8").decode( | |
| 261 | + "unicode_escape" | |
| 262 | + )[1:-1] | |
| 263 | + table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode( | |
| 264 | + "unicode_escape" | |
| 265 | + )[1:-1] | |
| 266 | + | |
| 267 | + # 为每个表生成个6个基本部分 | |
| 230 | 268 | create = self.gen_create(table_ddl) |
| 231 | 269 | pk = self.gen_pk(table_name) |
| 270 | + uk = self.gen_uk(table_ddl) | |
| 232 | 271 | index = self.gen_index(table_ddl) |
| 233 | - comment = self.gen_comment(table_sql, table_name) | |
| 272 | + comment = self.gen_comment(table_ddl) | |
| 234 | 273 | inserts = self.gen_insert(table_name) |
| 235 | 274 | |
| 236 | 275 | # 组合当前表的DDL脚本 |
| ... | ... | @@ -238,6 +277,8 @@ class Convertor(ABC): |
| 238 | 277 | |
| 239 | 278 | {pk} |
| 240 | 279 | |
| 280 | +{uk} | |
| 281 | + | |
| 241 | 282 | {index} |
| 242 | 283 | |
| 243 | 284 | {comment} |
| ... | ... | @@ -267,17 +308,19 @@ class PostgreSQLConvertor(Convertor): |
| 267 | 308 | |
| 268 | 309 | if type == "varchar": |
| 269 | 310 | return f"varchar({size})" |
| 270 | - if type == "int": | |
| 311 | + if type in ("int", "int unsigned"): | |
| 271 | 312 | return "int4" |
| 272 | - if type == "bigint" or type == "bigint unsigned": | |
| 313 | + if type in ("bigint", "bigint unsigned"): | |
| 273 | 314 | return "int8" |
| 274 | 315 | if type == "datetime": |
| 275 | 316 | return "timestamp" |
| 317 | + if type == "timestamp": | |
| 318 | + return f"timestamp({size})" | |
| 276 | 319 | if type == "bit": |
| 277 | 320 | return "bool" |
| 278 | 321 | if type in ("tinyint", "smallint"): |
| 279 | 322 | return "int2" |
| 280 | - if type == "text": | |
| 323 | + if type in ("text", "longtext"): | |
| 281 | 324 | return "text" |
| 282 | 325 | if type in ("blob", "mediumblob"): |
| 283 | 326 | return "bytea" |
| ... | ... | @@ -316,18 +359,22 @@ CREATE TABLE {table_name} ( |
| 316 | 359 | def gen_index(self, ddl: Dict) -> str: |
| 317 | 360 | return "\n".join(f"{script};" for script in self.index(ddl)) |
| 318 | 361 | |
| 319 | - def gen_comment(self, table_sql: str, table_name: str) -> str: | |
| 362 | + def gen_comment(self, table_ddl: Dict) -> str: | |
| 320 | 363 | """生成字段及表的注释""" |
| 321 | 364 | |
| 322 | 365 | script = "" |
| 323 | - for field, comment_string in self.filed_comments(table_sql): | |
| 366 | + for column in table_ddl["columns"]: | |
| 367 | + table_comment = column["comment"] | |
| 324 | 368 | script += ( |
| 325 | - f"COMMENT ON COLUMN {table_name}.{field} IS '{comment_string}';" + "\n" | |
| 369 | + f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" | |
| 370 | + + "\n" | |
| 326 | 371 | ) |
| 327 | 372 | |
| 328 | - table_comment = self.table_comment(table_sql) | |
| 373 | + table_comment = table_ddl["comment"] | |
| 329 | 374 | if table_comment: |
| 330 | - script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" | |
| 375 | + script += ( | |
| 376 | + f"COMMENT ON TABLE {table_ddl['table_name']} IS '{table_comment}';\n" | |
| 377 | + ) | |
| 331 | 378 | |
| 332 | 379 | return script |
| 333 | 380 | |
| ... | ... | @@ -335,6 +382,15 @@ CREATE TABLE {table_name} ( |
| 335 | 382 | """生成主键定义""" |
| 336 | 383 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" |
| 337 | 384 | |
| 385 | + def gen_uk(self, table_ddl: Dict) -> str: | |
| 386 | + script = "" | |
| 387 | + uk_list = list(Convertor.unique_index(table_ddl)) | |
| 388 | + for idx, (table_name, _, uk_columns) in enumerate(uk_list, 1): | |
| 389 | + uk_name = f"uk_{table_name}_{idx:02d}" | |
| 390 | + script += f"CREATE UNIQUE INDEX {uk_name} ON {table_name} ({', '.join(uk_columns)});\n" | |
| 391 | + | |
| 392 | + return script | |
| 393 | + | |
| 338 | 394 | def gen_insert(self, table_name: str) -> str: |
| 339 | 395 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| 340 | 396 | |
| ... | ... | @@ -393,17 +449,19 @@ class OracleConvertor(Convertor): |
| 393 | 449 | |
| 394 | 450 | if type == "varchar": |
| 395 | 451 | return f"varchar2({size if size < 4000 else 4000})" |
| 396 | - if type == "int": | |
| 452 | + if type in ("int", "int unsigned"): | |
| 397 | 453 | return "number" |
| 398 | 454 | if type == "bigint" or type == "bigint unsigned": |
| 399 | 455 | return "number" |
| 400 | 456 | if type == "datetime": |
| 401 | 457 | return "date" |
| 458 | + if type == "timestamp": | |
| 459 | + return f"timestamp({size})" | |
| 402 | 460 | if type == "bit": |
| 403 | 461 | return "number(1,0)" |
| 404 | 462 | if type in ("tinyint", "smallint"): |
| 405 | 463 | return "smallint" |
| 406 | - if type == "text": | |
| 464 | + if type in ("text", "longtext"): | |
| 407 | 465 | return "clob" |
| 408 | 466 | if type in ("blob", "mediumblob"): |
| 409 | 467 | return "blob" |
| ... | ... | @@ -423,6 +481,8 @@ class OracleConvertor(Convertor): |
| 423 | 481 | type = col["type"].lower() |
| 424 | 482 | full_type = self.translate_type(type, col["size"]) |
| 425 | 483 | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| 484 | + # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL | |
| 485 | + nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable | |
| 426 | 486 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 427 | 487 | # Oracle 中 size 不能作为字段名 |
| 428 | 488 | field_name = '"size"' if name == "size" else name |
| ... | ... | @@ -447,16 +507,20 @@ CREATE TABLE {table_name} ( |
| 447 | 507 | def gen_index(self, ddl: Dict) -> str: |
| 448 | 508 | return "\n".join(f"{script};" for script in self.index(ddl)) |
| 449 | 509 | |
| 450 | - def gen_comment(self, table_sql: str, table_name: str) -> str: | |
| 510 | + def gen_comment(self, table_ddl: Dict) -> str: | |
| 451 | 511 | script = "" |
| 452 | - for field, comment_string in self.filed_comments(table_sql): | |
| 512 | + for column in table_ddl["columns"]: | |
| 513 | + table_comment = column["comment"] | |
| 453 | 514 | script += ( |
| 454 | - f"COMMENT ON COLUMN {table_name}.{field} IS '{comment_string}';" + "\n" | |
| 515 | + f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" | |
| 516 | + + "\n" | |
| 455 | 517 | ) |
| 456 | 518 | |
| 457 | - table_comment = self.table_comment(table_sql) | |
| 519 | + table_comment = table_ddl["comment"] | |
| 458 | 520 | if table_comment: |
| 459 | - script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" | |
| 521 | + script += ( | |
| 522 | + f"COMMENT ON TABLE {table_ddl['table_name']} IS '{table_comment}';\n" | |
| 523 | + ) | |
| 460 | 524 | |
| 461 | 525 | return script |
| 462 | 526 | |
| ... | ... | @@ -464,6 +528,15 @@ CREATE TABLE {table_name} ( |
| 464 | 528 | """生成主键定义""" |
| 465 | 529 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" |
| 466 | 530 | |
| 531 | + def gen_uk(self, table_ddl: Dict) -> str: | |
| 532 | + script = "" | |
| 533 | + uk_list = list(Convertor.unique_index(table_ddl)) | |
| 534 | + for idx, (table_name, _, uk_columns) in enumerate(uk_list, 1): | |
| 535 | + uk_name = f"uk_{table_name}_{idx:02d}" | |
| 536 | + script += f"CREATE UNIQUE INDEX {uk_name} ON {table_name} ({', '.join(uk_columns)});\n" | |
| 537 | + | |
| 538 | + return script | |
| 539 | + | |
| 467 | 540 | def gen_index(self, ddl: Dict) -> str: |
| 468 | 541 | return "\n".join(f"{script};" for script in self.index(ddl)) |
| 469 | 542 | |
| ... | ... | @@ -521,17 +594,17 @@ class SQLServerConvertor(Convertor): |
| 521 | 594 | |
| 522 | 595 | if type == "varchar": |
| 523 | 596 | return f"nvarchar({size if size < 4000 else 4000})" |
| 524 | - if type == "int": | |
| 597 | + if type in ("int", "int unsigned"): | |
| 525 | 598 | return "int" |
| 526 | - if type == "bigint" or type == "bigint unsigned": | |
| 599 | + if type in ("bigint", "bigint unsigned"): | |
| 527 | 600 | return "bigint" |
| 528 | - if type == "datetime": | |
| 601 | + if type in ("datetime", "timestamp"): | |
| 529 | 602 | return "datetime2" |
| 530 | 603 | if type == "bit": |
| 531 | 604 | return "varchar(1)" |
| 532 | 605 | if type in ("tinyint", "smallint"): |
| 533 | 606 | return "tinyint" |
| 534 | - if type == "text": | |
| 607 | + if type in ("text", "longtext"): | |
| 535 | 608 | return "nvarchar(max)" |
| 536 | 609 | if type in ("blob", "mediumblob"): |
| 537 | 610 | return "varbinary(max)" |
| ... | ... | @@ -571,14 +644,18 @@ GO""" |
| 571 | 644 | |
| 572 | 645 | return script |
| 573 | 646 | |
| 574 | - def gen_comment(self, table_sql: str, table_name: str) -> str: | |
| 647 | + def gen_comment(self, table_ddl: Dict) -> str: | |
| 575 | 648 | """生成字段及表的注释""" |
| 576 | 649 | |
| 577 | 650 | script = "" |
| 651 | + table_name = table_ddl["table_name"] | |
| 652 | + | |
| 653 | + for column in table_ddl["columns"]: | |
| 654 | + column_comment = column["comment"] | |
| 655 | + field = column["name"] | |
| 578 | 656 | |
| 579 | - for field, comment_string in self.filed_comments(table_sql): | |
| 580 | 657 | script += f"""EXEC sp_addextendedproperty |
| 581 | - 'MS_Description', N'{comment_string}', | |
| 658 | + 'MS_Description', N'{column_comment}', | |
| 582 | 659 | 'SCHEMA', N'dbo', |
| 583 | 660 | 'TABLE', N'{table_name}', |
| 584 | 661 | 'COLUMN', N'{field}' |
| ... | ... | @@ -586,7 +663,7 @@ GO |
| 586 | 663 | |
| 587 | 664 | """ |
| 588 | 665 | |
| 589 | - table_comment = self.table_comment(table_sql) | |
| 666 | + table_comment = table_ddl["comment"] | |
| 590 | 667 | if table_comment: |
| 591 | 668 | script += f"""EXEC sp_addextendedproperty |
| 592 | 669 | 'MS_Description', N'{table_comment}', |
| ... | ... | @@ -601,6 +678,15 @@ GO |
| 601 | 678 | """生成主键定义""" |
| 602 | 679 | return "" |
| 603 | 680 | |
| 681 | + def gen_uk(self, table_ddl: Dict) -> str: | |
| 682 | + script = "" | |
| 683 | + uk_list = list(Convertor.unique_index(table_ddl)) | |
| 684 | + for idx, (table_name, _, uk_columns) in enumerate(uk_list, 1): | |
| 685 | + uk_name = f"uk_{table_name}_{idx:02d}" | |
| 686 | + script += f"CREATE UNIQUE INDEX {uk_name} ON {table_name} ({', '.join(uk_columns)})\nGO" | |
| 687 | + | |
| 688 | + return script | |
| 689 | + | |
| 604 | 690 | def gen_index(self, ddl: Dict) -> str: |
| 605 | 691 | """生成 index""" |
| 606 | 692 | return "\n".join(f"{script}\nGO" for script in self.index(ddl)) |
| ... | ... | @@ -674,22 +760,22 @@ class DM8Convertor(Convertor): |
| 674 | 760 | |
| 675 | 761 | if type == "varchar": |
| 676 | 762 | return f"varchar({size})" |
| 677 | - if type == "int": | |
| 763 | + if type in ("int", "int unsigned"): | |
| 678 | 764 | return "int" |
| 679 | - if type == "bigint" or type == "bigint unsigned": | |
| 765 | + if type in ("bigint", "bigint unsigned"): | |
| 680 | 766 | return "bigint" |
| 681 | 767 | if type == "datetime": |
| 682 | 768 | return "datetime" |
| 769 | + if type == "timestamp": | |
| 770 | + return f"timestamp({size})" | |
| 683 | 771 | if type == "bit": |
| 684 | 772 | return "bit" |
| 685 | 773 | if type in ("tinyint", "smallint"): |
| 686 | 774 | return "smallint" |
| 687 | - if type == "text": | |
| 775 | + if type in ("text", "longtext"): | |
| 688 | 776 | return "text" |
| 689 | - if type == "blob": | |
| 777 | + if type in ("blob", "mediumblob"): | |
| 690 | 778 | return "blob" |
| 691 | - if type == "mediumblob": | |
| 692 | - return "varchar(10240)" | |
| 693 | 779 | if type == "decimal": |
| 694 | 780 | return ( |
| 695 | 781 | f"decimal({','.join(str(s) for s in size)})" if len(size) else "decimal" |
| ... | ... | @@ -724,19 +810,20 @@ CREATE TABLE {table_name} ( |
| 724 | 810 | |
| 725 | 811 | return script |
| 726 | 812 | |
| 727 | - def gen_index(self, ddl: Dict) -> str: | |
| 728 | - return "\n".join(f"{script};" for script in self.index(ddl)) | |
| 729 | - | |
| 730 | - def gen_comment(self, table_sql: str, table_name: str) -> str: | |
| 813 | + def gen_comment(self, table_ddl: Dict) -> str: | |
| 731 | 814 | script = "" |
| 732 | - for field, comment_string in self.filed_comments(table_sql): | |
| 815 | + for column in table_ddl["columns"]: | |
| 816 | + table_comment = column["comment"] | |
| 733 | 817 | script += ( |
| 734 | - f"COMMENT ON COLUMN {table_name}.{field} IS '{comment_string}';" + "\n" | |
| 818 | + f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" | |
| 819 | + + "\n" | |
| 735 | 820 | ) |
| 736 | 821 | |
| 737 | - table_comment = self.table_comment(table_sql) | |
| 822 | + table_comment = table_ddl["comment"] | |
| 738 | 823 | if table_comment: |
| 739 | - script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" | |
| 824 | + script += ( | |
| 825 | + f"COMMENT ON TABLE {table_ddl['table_name']} IS '{table_comment}';\n" | |
| 826 | + ) | |
| 740 | 827 | |
| 741 | 828 | return script |
| 742 | 829 | |
| ... | ... | @@ -744,6 +831,15 @@ CREATE TABLE {table_name} ( |
| 744 | 831 | """生成主键定义""" |
| 745 | 832 | return "" |
| 746 | 833 | |
| 834 | + def gen_uk(self, table_ddl: Dict) -> str: | |
| 835 | + script = "" | |
| 836 | + uk_list = list(Convertor.unique_index(table_ddl)) | |
| 837 | + for idx, (table_name, _, uk_columns) in enumerate(uk_list, 1): | |
| 838 | + uk_name = f"uk_{table_name}_{idx:02d}" | |
| 839 | + script += f"CREATE UNIQUE INDEX {uk_name} ON {table_name} ({', '.join(uk_columns)});\n" | |
| 840 | + | |
| 841 | + return script | |
| 842 | + | |
| 747 | 843 | def gen_index(self, ddl: Dict) -> str: |
| 748 | 844 | return "\n".join(f"{script};" for script in self.index(ddl)) |
| 749 | 845 | |
| ... | ... | @@ -784,6 +880,8 @@ class KingbaseConvertor(PostgreSQLConvertor): |
| 784 | 880 | type = col["type"].lower() |
| 785 | 881 | full_type = self.translate_type(type, col["size"]) |
| 786 | 882 | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| 883 | + if full_type == "text": | |
| 884 | + nullable = "NULL" | |
| 787 | 885 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 788 | 886 | return f"{name} {full_type} {nullable} {default}" |
| 789 | 887 | ... | ... |