Commit b4a867b89c8b4dfe7c3622cb3aace1ff770ba068
Committed by
Gitee
!963 feat: 脚本兼容Python 3.9, 抽取更多公共代码; 更新使用文档
Merge pull request !963 from dhb52/feature/db
Showing
2 changed files
with
157 additions
and
151 deletions
sql/tools/README.md
| @@ -50,8 +50,16 @@ TODO 暂未支持 | @@ -50,8 +50,16 @@ TODO 暂未支持 | ||
| 50 | 50 | ||
| 51 | 使用方式如下: | 51 | 使用方式如下: |
| 52 | 52 | ||
| 53 | +安装依赖库 | ||
| 54 | + | ||
| 55 | +```bash | ||
| 56 | +pip install simple-ddl-parser | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +执行如下命令打印生成 postgresql 的脚本内容,其他可选参数有:oracle, sqlserver | ||
| 60 | + | ||
| 53 | ```Bash | 61 | ```Bash |
| 54 | -python3 convertor.py | 62 | +python3 convertor.py postgres |
| 55 | ``` | 63 | ``` |
| 56 | 64 | ||
| 57 | -然后,TODO | ||
| 58 | \ No newline at end of file | 65 | \ No newline at end of file |
| 66 | +程序将sql脚本打印到终端,可以重定向到临时文件tmp.sql, 确认无误后可以利用IDEA(专业版)进行格式化。 |
sql/tools/convertor.py
| @@ -6,11 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52) | @@ -6,11 +6,12 @@ Author: dhb52 (https://gitee.com/dhb52) | ||
| 6 | pip install simple-ddl-parser | 6 | pip install simple-ddl-parser |
| 7 | """ | 7 | """ |
| 8 | 8 | ||
| 9 | +import argparse | ||
| 9 | import pathlib | 10 | import pathlib |
| 10 | import re | 11 | import re |
| 11 | import time | 12 | import time |
| 12 | from abc import ABC, abstractmethod | 13 | from abc import ABC, abstractmethod |
| 13 | -from typing import Dict, Tuple | 14 | +from typing import Dict, Generator, Optional, Tuple, Union |
| 14 | 15 | ||
| 15 | from simple_ddl_parser import DDLParser | 16 | from simple_ddl_parser import DDLParser |
| 16 | 17 | ||
| @@ -60,12 +61,12 @@ class Convertor(ABC): | @@ -60,12 +61,12 @@ class Convertor(ABC): | ||
| 60 | self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content) | 61 | self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content) |
| 61 | 62 | ||
| 62 | @abstractmethod | 63 | @abstractmethod |
| 63 | - def translate_type(self, type: str, size: None | int | Tuple[int]) -> str: | 64 | + def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str: |
| 64 | """字段类型转换 | 65 | """字段类型转换 |
| 65 | 66 | ||
| 66 | Args: | 67 | Args: |
| 67 | type (str): 字段类型 | 68 | type (str): 字段类型 |
| 68 | - size (None | int | Tuple[int]): 字段长度描述, 如varchar(255), decimal(10,2) | 69 | + size (Optional[Union[int, Tuple[int]]]): 字段长度描述, 如varchar(255), decimal(10,2) |
| 69 | 70 | ||
| 70 | Returns: | 71 | Returns: |
| 71 | str: 类型定义 | 72 | str: 类型定义 |
| @@ -97,7 +98,7 @@ class Convertor(ABC): | @@ -97,7 +98,7 @@ class Convertor(ABC): | ||
| 97 | pass | 98 | pass |
| 98 | 99 | ||
| 99 | @abstractmethod | 100 | @abstractmethod |
| 100 | - def gen_index(self, table_ddl: Dict) -> str: | 101 | + def gen_index(self, ddl: Dict) -> str: |
| 101 | """生成索引定义 | 102 | """生成索引定义 |
| 102 | 103 | ||
| 103 | Args: | 104 | Args: |
| @@ -133,6 +134,55 @@ class Convertor(ABC): | @@ -133,6 +134,55 @@ class Convertor(ABC): | ||
| 133 | """ | 134 | """ |
| 134 | pass | 135 | pass |
| 135 | 136 | ||
| 137 | + @staticmethod | ||
| 138 | + def inserts(table_name: str, script_content: str) -> Generator: | ||
| 139 | + PREFIX = f"INSERT INTO `{table_name}`" | ||
| 140 | + | ||
| 141 | + # 收集 `table_name` 对应的 insert 语句 | ||
| 142 | + for line in script_content.split("\n"): | ||
| 143 | + if line.startswith(PREFIX): | ||
| 144 | + head, tail = line.replace(PREFIX, "").split(" VALUES ", maxsplit=1) | ||
| 145 | + head = head.strip().replace("`", "").lower() | ||
| 146 | + tail = tail.strip().replace(r"\"", '"') | ||
| 147 | + # tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'") | ||
| 148 | + yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}" | ||
| 149 | + | ||
| 150 | + @staticmethod | ||
| 151 | + def index(ddl: Dict) -> Generator: | ||
| 152 | + """生成索引定义 | ||
| 153 | + | ||
| 154 | + Args: | ||
| 155 | + ddl (Dict): 表DDL | ||
| 156 | + | ||
| 157 | + Yields: | ||
| 158 | + Generator[str]: create index 语句 | ||
| 159 | + """ | ||
| 160 | + | ||
| 161 | + def generate_columns(columns): | ||
| 162 | + keys = [ | ||
| 163 | + f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}" | ||
| 164 | + for col in columns[0] | ||
| 165 | + ] | ||
| 166 | + return ", ".join(keys) | ||
| 167 | + | ||
| 168 | + for no, index in enumerate(ddl["index"], 1): | ||
| 169 | + columns = generate_columns(index["columns"]) | ||
| 170 | + table_name = ddl["table_name"].lower() | ||
| 171 | + yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" | ||
| 172 | + | ||
| 173 | + @staticmethod | ||
| 174 | + def filed_comments(table_sql: str) -> Generator: | ||
| 175 | + for line in table_sql.split("\n"): | ||
| 176 | + match = re.match(r"^`([^`]+)`.* COMMENT '([^']+)'", line.strip()) | ||
| 177 | + if match: | ||
| 178 | + field = match.group(1) | ||
| 179 | + comment_string = match.group(2).replace("\\n", "\n") | ||
| 180 | + yield field, comment_string | ||
| 181 | + | ||
| 182 | + def table_comment(self, table_sql: str) -> str: | ||
| 183 | + match = re.search(r"COMMENT \= '([^']+)';", table_sql) | ||
| 184 | + return match.group(1) if match else None | ||
| 185 | + | ||
| 136 | def print(self): | 186 | def print(self): |
| 137 | """打印转换后的sql脚本到终端""" | 187 | """打印转换后的sql脚本到终端""" |
| 138 | print( | 188 | print( |
| @@ -192,7 +242,7 @@ class PostgreSQLConvertor(Convertor): | @@ -192,7 +242,7 @@ class PostgreSQLConvertor(Convertor): | ||
| 192 | def __init__(self, src): | 242 | def __init__(self, src): |
| 193 | super().__init__(src, "PostgreSQL") | 243 | super().__init__(src, "PostgreSQL") |
| 194 | 244 | ||
| 195 | - def translate_type(self, type, size): | 245 | + def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]): |
| 196 | """类型转换""" | 246 | """类型转换""" |
| 197 | 247 | ||
| 198 | type = type.lower() | 248 | type = type.lower() |
| @@ -234,27 +284,30 @@ class PostgreSQLConvertor(Convertor): | @@ -234,27 +284,30 @@ class PostgreSQLConvertor(Convertor): | ||
| 234 | 284 | ||
| 235 | table_name = ddl["table_name"].lower() | 285 | table_name = ddl["table_name"].lower() |
| 236 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] | 286 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| 287 | + filed_def_list = ",\n ".join(columns) | ||
| 237 | script = f"""-- ---------------------------- | 288 | script = f"""-- ---------------------------- |
| 238 | -- Table structure for {table_name} | 289 | -- Table structure for {table_name} |
| 239 | -- ---------------------------- | 290 | -- ---------------------------- |
| 240 | DROP TABLE IF EXISTS {table_name}; | 291 | DROP TABLE IF EXISTS {table_name}; |
| 241 | CREATE TABLE {table_name} ( | 292 | CREATE TABLE {table_name} ( |
| 242 | - {',\n '.join(columns)} | 293 | + {filed_def_list} |
| 243 | );""" | 294 | );""" |
| 244 | 295 | ||
| 245 | return script | 296 | return script |
| 246 | 297 | ||
| 247 | - def gen_comment(self, table_sql, table_name) -> str: | 298 | + def gen_index(self, ddl: Dict) -> str: |
| 299 | + return "\n".join(f"{script};" for script in self.index(ddl)) | ||
| 300 | + | ||
| 301 | + def gen_comment(self, table_sql: str, table_name: str) -> str: | ||
| 248 | """生成字段及表的注释""" | 302 | """生成字段及表的注释""" |
| 249 | 303 | ||
| 250 | script = "" | 304 | script = "" |
| 251 | - for line in table_sql.split("\n"): | ||
| 252 | - match = re.match(r"^`([^`]+)`.* COMMENT '([^']+)'", line.strip()) | ||
| 253 | - if match: | ||
| 254 | - script += f"COMMENT ON COLUMN {table_name}.{match.group(1)} IS '{match.group(2).replace('\\n', '\n')}';\n" | 305 | + for field, comment_string in self.filed_comments(table_sql): |
| 306 | + script += ( | ||
| 307 | + f"COMMENT ON COLUMN {table_name}.{field} IS '{comment_string}';" + "\n" | ||
| 308 | + ) | ||
| 255 | 309 | ||
| 256 | - match = re.search(r"COMMENT \= '([^']+)';", table_sql) | ||
| 257 | - table_comment = match.group(1) if match else None | 310 | + table_comment = self.table_comment(table_sql) |
| 258 | if table_comment: | 311 | if table_comment: |
| 259 | script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" | 312 | script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" |
| 260 | 313 | ||
| @@ -264,53 +317,21 @@ CREATE TABLE {table_name} ( | @@ -264,53 +317,21 @@ CREATE TABLE {table_name} ( | ||
| 264 | """生成主键定义""" | 317 | """生成主键定义""" |
| 265 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" | 318 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" |
| 266 | 319 | ||
| 267 | - def gen_index(self, ddl) -> str: | ||
| 268 | - """生成 index""" | ||
| 269 | - | ||
| 270 | - def generate_columns(columns): | ||
| 271 | - keys = [ | ||
| 272 | - f"{col['name'].lower()}{" " + col['order'].lower() if col['order'] != 'ASC' else ''}" | ||
| 273 | - for col in columns[0] | ||
| 274 | - ] | ||
| 275 | - return ", ".join(keys) | ||
| 276 | - | ||
| 277 | - script = "" | ||
| 278 | - for no, index in enumerate(ddl["index"], 1): | ||
| 279 | - columns = generate_columns(index["columns"]) | ||
| 280 | - table_name = ddl["table_name"].lower() | ||
| 281 | - script += ( | ||
| 282 | - f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns});\n" | ||
| 283 | - ) | ||
| 284 | - | ||
| 285 | - return script | ||
| 286 | - | ||
| 287 | - def gen_insert(self, table_name) -> str: | 320 | + def gen_insert(self, table_name: str) -> str: |
| 288 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" | 321 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| 289 | 322 | ||
| 290 | - PREFIX = f"INSERT INTO `{table_name}`" | ||
| 291 | - | ||
| 292 | - # 收集 `table_name` 对应的 insert 语句 | ||
| 293 | - inserts = [] | ||
| 294 | - for line in self.content.split("\n"): | ||
| 295 | - if line.startswith(PREFIX): | ||
| 296 | - head, tail = line.replace(PREFIX, "").split(" VALUES ", maxsplit=1) | ||
| 297 | - head = head.strip().replace("`", "").lower() | ||
| 298 | - tail = tail.strip().replace(r"\"", '"') | ||
| 299 | - script = f"INSERT INTO {table_name.lower()} {head} VALUES {tail}" | ||
| 300 | - # bit(1)数据转换 | ||
| 301 | - script = script.replace("b'0'", "'0'").replace("b'1'", "'1'") | ||
| 302 | - inserts.append(script) | ||
| 303 | - | 323 | + inserts = list(Convertor.inserts(table_name, self.content)) |
| 304 | ## 生成 insert 脚本 | 324 | ## 生成 insert 脚本 |
| 305 | script = "" | 325 | script = "" |
| 306 | last_id = 0 | 326 | last_id = 0 |
| 307 | if inserts: | 327 | if inserts: |
| 328 | + inserts_lines = "\n".join(inserts) | ||
| 308 | script += f"""\n\n-- ---------------------------- | 329 | script += f"""\n\n-- ---------------------------- |
| 309 | -- Records of {table_name.lower()} | 330 | -- Records of {table_name.lower()} |
| 310 | -- ---------------------------- | 331 | -- ---------------------------- |
| 311 | -- @formatter:off | 332 | -- @formatter:off |
| 312 | BEGIN; | 333 | BEGIN; |
| 313 | -{'\n'.join(inserts)} | 334 | +{inserts_lines} |
| 314 | COMMIT; | 335 | COMMIT; |
| 315 | -- @formatter:on""" | 336 | -- @formatter:on""" |
| 316 | match = re.search(r"VALUES \((\d+),", inserts[-1]) | 337 | match = re.search(r"VALUES \((\d+),", inserts[-1]) |
| @@ -332,7 +353,7 @@ class OracleConvertor(Convertor): | @@ -332,7 +353,7 @@ class OracleConvertor(Convertor): | ||
| 332 | def __init__(self, src): | 353 | def __init__(self, src): |
| 333 | super().__init__(src, "Oracle") | 354 | super().__init__(src, "Oracle") |
| 334 | 355 | ||
| 335 | - def translate_type(self, type, size: None | int | Tuple[int]): | 356 | + def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]): |
| 336 | """类型转换""" | 357 | """类型转换""" |
| 337 | type = type.lower() | 358 | type = type.lower() |
| 338 | 359 | ||
| @@ -369,15 +390,19 @@ class OracleConvertor(Convertor): | @@ -369,15 +390,19 @@ class OracleConvertor(Convertor): | ||
| 369 | full_type = self.translate_type(type, col["size"]) | 390 | full_type = self.translate_type(type, col["size"]) |
| 370 | nullable = "NULL" if col["nullable"] else "NOT NULL" | 391 | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| 371 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" | 392 | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| 372 | - return f"{'\"size\"' if name == "size" else name } {full_type} {default} {nullable}" | 393 | + # Oracle 中 size 不能作为字段名 |
| 394 | + field_name = '"size"' if name == "size" else name | ||
| 395 | + # Oracle DEFAULT 定义在 NULLABLE 之前 | ||
| 396 | + return f"{field_name} {full_type} {default} {nullable}" | ||
| 373 | 397 | ||
| 374 | table_name = ddl["table_name"].lower() | 398 | table_name = ddl["table_name"].lower() |
| 375 | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] | 399 | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] |
| 400 | + field_def_list = ",\n ".join(columns) | ||
| 376 | script = f"""-- ---------------------------- | 401 | script = f"""-- ---------------------------- |
| 377 | -- Table structure for {table_name} | 402 | -- Table structure for {table_name} |
| 378 | -- ---------------------------- | 403 | -- ---------------------------- |
| 379 | -CREATE TABLE {ddl['table_name'].lower()} ( | ||
| 380 | - {',\n '.join(columns)} | 404 | +CREATE TABLE {table_name} ( |
| 405 | + {field_def_list} | ||
| 381 | );""" | 406 | );""" |
| 382 | 407 | ||
| 383 | # oracle INSERT '' 不能通过 NOT NULL 校验 | 408 | # oracle INSERT '' 不能通过 NOT NULL 校验 |
| @@ -385,72 +410,51 @@ CREATE TABLE {ddl['table_name'].lower()} ( | @@ -385,72 +410,51 @@ CREATE TABLE {ddl['table_name'].lower()} ( | ||
| 385 | 410 | ||
| 386 | return script | 411 | return script |
| 387 | 412 | ||
| 388 | - def gen_comment(self, table_sql, table_name) -> str: | 413 | + def gen_index(self, ddl: Dict) -> str: |
| 414 | + return "\n".join(f"{script};" for script in self.index(ddl)) | ||
| 415 | + | ||
| 416 | + def gen_comment(self, table_sql: str, table_name: str) -> str: | ||
| 389 | script = "" | 417 | script = "" |
| 390 | - for line in table_sql.split("\n"): | ||
| 391 | - match = re.search(r"`([^`]+)`.* COMMENT '([^']+)'", line) | ||
| 392 | - if match: | ||
| 393 | - script += f"COMMENT ON COLUMN {table_name}.{match.group(1)} IS '{match.group(2).replace('\\n', '\n')}';\n" | 418 | + for field, comment_string in self.filed_comments(table_sql): |
| 419 | + script += ( | ||
| 420 | + f"COMMENT ON COLUMN {table_name}.{field} IS '{comment_string}';" + "\n" | ||
| 421 | + ) | ||
| 394 | 422 | ||
| 395 | - match = re.search(r"COMMENT \= '([^']+)';", table_sql) | ||
| 396 | - table_comment = match.group(1) if match else None | 423 | + table_comment = self.table_comment(table_sql) |
| 397 | if table_comment: | 424 | if table_comment: |
| 398 | - script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';" | 425 | + script += f"COMMENT ON TABLE {table_name} IS '{table_comment}';\n" |
| 399 | 426 | ||
| 400 | return script | 427 | return script |
| 401 | 428 | ||
| 402 | - def gen_pk(self, table_name) -> str: | 429 | + def gen_pk(self, table_name: str) -> str: |
| 403 | """生成主键定义""" | 430 | """生成主键定义""" |
| 404 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" | 431 | return f"ALTER TABLE {table_name} ADD CONSTRAINT pk_{table_name} PRIMARY KEY (id);\n" |
| 405 | 432 | ||
| 406 | - def gen_index(self, table_ddl) -> str: | ||
| 407 | - """生成 INDEX 定义""" | ||
| 408 | - | ||
| 409 | - def generate_columns(columns): | ||
| 410 | - keys = [ | ||
| 411 | - f"{col['name'].lower()}{" " + col['order'].lower() if col['order'] != 'ASC' else ''}" | ||
| 412 | - for col in columns[0] | ||
| 413 | - ] | ||
| 414 | - return ", ".join(keys) | ||
| 415 | - | ||
| 416 | - script = "" | ||
| 417 | - for no, index in enumerate(table_ddl["index"], 1): | ||
| 418 | - columns = generate_columns(index["columns"]) | ||
| 419 | - table_name = table_ddl["table_name"].lower() | ||
| 420 | - script += ( | ||
| 421 | - f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns});\n" | ||
| 422 | - ) | ||
| 423 | - return script | 433 | + def gen_index(self, ddl: Dict) -> str: |
| 434 | + return "\n".join(f"{script};" for script in self.index(ddl)) | ||
| 424 | 435 | ||
| 425 | - def gen_insert(self, table_name) -> str: | 436 | + def gen_insert(self, table_name: str) -> str: |
| 426 | """拷贝 INSERT 语句""" | 437 | """拷贝 INSERT 语句""" |
| 427 | - PREFIX = f"INSERT INTO `{table_name}`" | ||
| 428 | inserts = [] | 438 | inserts = [] |
| 429 | - for line in self.content.split("\n"): | ||
| 430 | - if line.startswith(PREFIX): | ||
| 431 | - head, tail = line.replace(PREFIX, "").split(" VALUES ", maxsplit=1) | ||
| 432 | - head = head.strip().replace("`", "").lower() | ||
| 433 | - tail = tail.strip().replace(r"\"", '"') | ||
| 434 | - script = f"INSERT INTO {table_name.lower()} {head} VALUES {tail}" | ||
| 435 | - # bit(1)数据转换 | ||
| 436 | - script = script.replace("b'0'", "'0'").replace("b'1'", "'1'") | ||
| 437 | - # 对日期数据添加 TO_DATE 转换 | ||
| 438 | - script = re.sub( | ||
| 439 | - r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", | ||
| 440 | - r"to_date(\g<1>, 'SYYYY-MM-DD HH24:MI:SS')", | ||
| 441 | - script, | ||
| 442 | - ) | ||
| 443 | - inserts.append(script) | 439 | + for insert_script in Convertor.inserts(table_name, self.content): |
| 440 | + # 对日期数据添加 TO_DATE 转换 | ||
| 441 | + insert_script = re.sub( | ||
| 442 | + r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", | ||
| 443 | + r"to_date(\g<1>, 'SYYYY-MM-DD HH24:MI:SS')", | ||
| 444 | + insert_script, | ||
| 445 | + ) | ||
| 446 | + inserts.append(insert_script) | ||
| 444 | 447 | ||
| 445 | ## 生成 insert 脚本 | 448 | ## 生成 insert 脚本 |
| 446 | script = "" | 449 | script = "" |
| 447 | last_id = 0 | 450 | last_id = 0 |
| 448 | if inserts: | 451 | if inserts: |
| 452 | + inserts_lines = "\n".join(inserts) | ||
| 449 | script += f"""\n\n-- ---------------------------- | 453 | script += f"""\n\n-- ---------------------------- |
| 450 | -- Records of {table_name.lower()} | 454 | -- Records of {table_name.lower()} |
| 451 | -- ---------------------------- | 455 | -- ---------------------------- |
| 452 | -- @formatter:off | 456 | -- @formatter:off |
| 453 | -{'\n'.join(inserts)} | 457 | +{inserts_lines} |
| 454 | COMMIT; | 458 | COMMIT; |
| 455 | -- @formatter:on""" | 459 | -- @formatter:on""" |
| 456 | match = re.search(r"VALUES \((\d+),", inserts[-1]) | 460 | match = re.search(r"VALUES \((\d+),", inserts[-1]) |
| @@ -476,7 +480,7 @@ class SQLServerConvertor(Convertor): | @@ -476,7 +480,7 @@ class SQLServerConvertor(Convertor): | ||
| 476 | def __init__(self, src): | 480 | def __init__(self, src): |
| 477 | super().__init__(src, "Microsoft SQL Server") | 481 | super().__init__(src, "Microsoft SQL Server") |
| 478 | 482 | ||
| 479 | - def translate_type(self, type, size): | 483 | + def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]): |
| 480 | """类型转换""" | 484 | """类型转换""" |
| 481 | 485 | ||
| 482 | type = type.lower() | 486 | type = type.lower() |
| @@ -507,7 +511,7 @@ class SQLServerConvertor(Convertor): | @@ -507,7 +511,7 @@ class SQLServerConvertor(Convertor): | ||
| 507 | 511 | ||
| 508 | def _generate_column(col): | 512 | def _generate_column(col): |
| 509 | name = col["name"].lower() | 513 | name = col["name"].lower() |
| 510 | - if name == 'id': | 514 | + if name == "id": |
| 511 | return "id bigint NOT NULL PRIMARY KEY IDENTITY" | 515 | return "id bigint NOT NULL PRIMARY KEY IDENTITY" |
| 512 | if name == "deleted": | 516 | if name == "deleted": |
| 513 | return "deleted bit DEFAULT 0 NOT NULL" | 517 | return "deleted bit DEFAULT 0 NOT NULL" |
| @@ -520,35 +524,34 @@ class SQLServerConvertor(Convertor): | @@ -520,35 +524,34 @@ class SQLServerConvertor(Convertor): | ||
| 520 | 524 | ||
| 521 | table_name = ddl["table_name"].lower() | 525 | table_name = ddl["table_name"].lower() |
| 522 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] | 526 | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| 527 | + filed_def_list = ",\n ".join(columns) | ||
| 523 | script = f"""-- ---------------------------- | 528 | script = f"""-- ---------------------------- |
| 524 | -- Table structure for {table_name} | 529 | -- Table structure for {table_name} |
| 525 | -- ---------------------------- | 530 | -- ---------------------------- |
| 526 | DROP TABLE IF EXISTS {table_name}; | 531 | DROP TABLE IF EXISTS {table_name}; |
| 527 | CREATE TABLE {table_name} ( | 532 | CREATE TABLE {table_name} ( |
| 528 | - {',\n '.join(columns)} | 533 | + {filed_def_list} |
| 529 | ) | 534 | ) |
| 530 | GO""" | 535 | GO""" |
| 531 | 536 | ||
| 532 | return script | 537 | return script |
| 533 | 538 | ||
| 534 | - def gen_comment(self, table_sql, table_name) -> str: | 539 | + def gen_comment(self, table_sql: str, table_name: str) -> str: |
| 535 | """生成字段及表的注释""" | 540 | """生成字段及表的注释""" |
| 536 | 541 | ||
| 537 | script = "" | 542 | script = "" |
| 538 | - for line in table_sql.split("\n"): | ||
| 539 | - match = re.match(r"^`([^`]+)`.* COMMENT '([^']+)'", line.strip()) | ||
| 540 | - if match: | ||
| 541 | - script += f"""EXEC sp_addextendedproperty | ||
| 542 | - 'MS_Description', N'{match.group(2).replace('\\n', '\n')}', | 543 | + |
| 544 | + for field, comment_string in self.filed_comments(table_sql): | ||
| 545 | + script += f"""EXEC sp_addextendedproperty | ||
| 546 | + 'MS_Description', N'{comment_string}', | ||
| 543 | 'SCHEMA', N'dbo', | 547 | 'SCHEMA', N'dbo', |
| 544 | 'TABLE', N'{table_name}', | 548 | 'TABLE', N'{table_name}', |
| 545 | - 'COLUMN', N'{match.group(1)}' | 549 | + 'COLUMN', N'{field}' |
| 546 | GO | 550 | GO |
| 547 | 551 | ||
| 548 | """ | 552 | """ |
| 549 | 553 | ||
| 550 | - match = re.search(r"COMMENT \= '([^']+)';", table_sql) | ||
| 551 | - table_comment = match.group(1) if match else None | 554 | + table_comment = self.table_comment(table_sql) |
| 552 | if table_comment: | 555 | if table_comment: |
| 553 | script += f"""EXEC sp_addextendedproperty | 556 | script += f"""EXEC sp_addextendedproperty |
| 554 | 'MS_Description', N'{table_comment}', | 557 | 'MS_Description', N'{table_comment}', |
| @@ -557,55 +560,34 @@ GO | @@ -557,55 +560,34 @@ GO | ||
| 557 | GO | 560 | GO |
| 558 | 561 | ||
| 559 | """ | 562 | """ |
| 560 | - | ||
| 561 | return script | 563 | return script |
| 562 | 564 | ||
| 563 | - def gen_pk(self, table_name) -> str: | 565 | + def gen_pk(self, table_name: str) -> str: |
| 564 | """生成主键定义""" | 566 | """生成主键定义""" |
| 565 | return "" | 567 | return "" |
| 566 | 568 | ||
| 567 | - def gen_index(self, ddl) -> str: | 569 | + def gen_index(self, ddl: Dict) -> str: |
| 568 | """生成 index""" | 570 | """生成 index""" |
| 571 | + return "\n".join(f"{script}\nGO" for script in self.index(ddl)) | ||
| 569 | 572 | ||
| 570 | - def generate_columns(columns): | ||
| 571 | - keys = [ | ||
| 572 | - f"{col['name'].lower()}{" " + col['order'].lower() if col['order'] != 'ASC' else ''}" | ||
| 573 | - for col in columns[0] | ||
| 574 | - ] | ||
| 575 | - return ", ".join(keys) | ||
| 576 | - | ||
| 577 | - script = "" | ||
| 578 | - for no, index in enumerate(ddl["index"], 1): | ||
| 579 | - columns = generate_columns(index["columns"]) | ||
| 580 | - table_name = ddl["table_name"].lower() | ||
| 581 | - script += f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})\nGO\n" | ||
| 582 | - | ||
| 583 | - return script | ||
| 584 | - | ||
| 585 | - def gen_insert(self, table_name) -> str: | 573 | + def gen_insert(self, table_name: str) -> str: |
| 586 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" | 574 | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| 587 | 575 | ||
| 588 | - PREFIX = f"INSERT INTO `{table_name}`" | ||
| 589 | - | ||
| 590 | # 收集 `table_name` 对应的 insert 语句 | 576 | # 收集 `table_name` 对应的 insert 语句 |
| 591 | inserts = [] | 577 | inserts = [] |
| 592 | - for line in self.content.split("\n"): | ||
| 593 | - if line.startswith(PREFIX): | ||
| 594 | - head, tail = line.replace(PREFIX, "").split(" VALUES ", maxsplit=1) | ||
| 595 | - head = head.strip().replace("`", "").lower() | ||
| 596 | - tail = tail.strip().replace(r"\"", '"') | ||
| 597 | - # SQLServer: 字符串前加N,hack,是否存在替换字符串内容的风险 | ||
| 598 | - tail = tail.replace(", '", ", N'").replace("VALUES ('", "VALUES (N')") | ||
| 599 | - script = f"INSERT INTO {table_name.lower()} {head} VALUES {tail}" | ||
| 600 | - # bit(1)数据转换 | ||
| 601 | - script = script.replace("b'0'", "'0'").replace("b'1'", "'1'") | ||
| 602 | - # 删除 insert 的结尾分号 | ||
| 603 | - script = re.sub(";$", r"\nGO", script) | ||
| 604 | - inserts.append(script) | 578 | + for insert_script in Convertor.inserts(table_name, self.content): |
| 579 | + # SQLServer: 字符串前加N,hack,是否存在替换字符串内容的风险 | ||
| 580 | + insert_script = insert_script.replace(", '", ", N'").replace( | ||
| 581 | + "VALUES ('", "VALUES (N')" | ||
| 582 | + ) | ||
| 583 | + # 删除 insert 的结尾分号 | ||
| 584 | + insert_script = re.sub(";$", r"\nGO", insert_script) | ||
| 585 | + inserts.append(insert_script) | ||
| 605 | 586 | ||
| 606 | ## 生成 insert 脚本 | 587 | ## 生成 insert 脚本 |
| 607 | script = "" | 588 | script = "" |
| 608 | if inserts: | 589 | if inserts: |
| 590 | + inserts_lines = "\n".join(inserts) | ||
| 609 | script += f"""\n\n-- ---------------------------- | 591 | script += f"""\n\n-- ---------------------------- |
| 610 | -- Records of {table_name.lower()} | 592 | -- Records of {table_name.lower()} |
| 611 | -- ---------------------------- | 593 | -- ---------------------------- |
| @@ -614,7 +596,7 @@ BEGIN TRANSACTION | @@ -614,7 +596,7 @@ BEGIN TRANSACTION | ||
| 614 | GO | 596 | GO |
| 615 | SET IDENTITY_INSERT {table_name.lower()} ON | 597 | SET IDENTITY_INSERT {table_name.lower()} ON |
| 616 | GO | 598 | GO |
| 617 | -{'\n'.join(inserts)} | 599 | +{inserts_lines} |
| 618 | SET IDENTITY_INSERT {table_name.lower()} OFF | 600 | SET IDENTITY_INSERT {table_name.lower()} OFF |
| 619 | GO | 601 | GO |
| 620 | COMMIT | 602 | COMMIT |
| @@ -625,10 +607,26 @@ GO | @@ -625,10 +607,26 @@ GO | ||
| 625 | 607 | ||
| 626 | 608 | ||
| 627 | def main(): | 609 | def main(): |
| 628 | - sql_file = pathlib.Path('../mysql/ruoyi-vue-pro.sql').resolve().as_posix() | ||
| 629 | - # convertor = PostgreSQLConvertor(sql_file) | ||
| 630 | - # convertor = OracleConvertor(sql_file) | ||
| 631 | - convertor = SQLServerConvertor(sql_file) | 610 | + parser = argparse.ArgumentParser(description="芋道系统数据库转换工具") |
| 611 | + parser.add_argument( | ||
| 612 | + "type", | ||
| 613 | + type=str, | ||
| 614 | + help="目标数据库类型", | ||
| 615 | + choices=["postgres", "oracle", "sqlserver"], | ||
| 616 | + ) | ||
| 617 | + args = parser.parse_args() | ||
| 618 | + | ||
| 619 | + sql_file = pathlib.Path("../mysql/ruoyi-vue-pro.sql").resolve().as_posix() | ||
| 620 | + convertor = None | ||
| 621 | + if args.type == "postgres": | ||
| 622 | + convertor = PostgreSQLConvertor(sql_file) | ||
| 623 | + elif args.type == "oracle": | ||
| 624 | + convertor = OracleConvertor(sql_file) | ||
| 625 | + elif args.type == "sqlserver": | ||
| 626 | + convertor = SQLServerConvertor(sql_file) | ||
| 627 | + else: | ||
| 628 | + raise NotImplementedError(f"不支持目标数据库类型: {args.type}") | ||
| 629 | + | ||
| 632 | convertor.print() | 630 | convertor.print() |
| 633 | 631 | ||
| 634 | 632 |