| | |
| | | uv run --with simple-ddl-parser convertor.py sqlserver ../mysql/ruoyi-vue-pro.sql > ../sqlserver/ruoyi-vue-pro.sql |
| | | uv run --with simple-ddl-parser convertor.py kingbase ../mysql/ruoyi-vue-pro.sql > ../kingbase/ruoyi-vue-pro.sql |
| | | uv run --with simple-ddl-parser convertor.py opengauss ../mysql/ruoyi-vue-pro.sql > ../opengauss/ruoyi-vue-pro.sql |
| | | uv run --with simple-ddl-parser convertor.py highgo ../mysql/ruoyi-vue-pro.sql > ../highgo/ruoyi-vue-pro.sql |
| | | uv run --with simple-ddl-parser convertor.py oracle ../mysql/ruoyi-vue-pro.sql > ../oracle/ruoyi-vue-pro.sql |
| | | uv run --with simple-ddl-parser convertor.py dm8 ../mysql/ruoyi-vue-pro.sql > ../dm/ruoyi-vue-pro-dm8.sql |
| | | """ |
| | |
| | | |
| | | |
| | | class Convertor(ABC): |
| | | # 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。 |
| | | reserved_column_names = set() |
| | | |
| | | def __init__(self, src: str, db_type) -> None: |
| | | self.src = src |
| | | self.db_type = db_type |
| | |
| | | """ |
| | | return "" |
| | | |
| | | def escape_column_name(self, name: str) -> str: |
| | | """转义目标库保留字列名,例如 Oracle / Kingbase 的 level。""" |
| | | |
| | | column_name = name.lower() |
| | | if column_name in self.reserved_column_names: |
| | | return f'"{column_name}"' |
| | | return column_name |
| | | |
| | | def escape_insert_columns(self, insert_script: str) -> str: |
| | | """INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。""" |
| | | |
| | | match = re.match( |
| | | r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)", |
| | | insert_script, |
| | | flags=re.IGNORECASE, |
| | | ) |
| | | if not match: |
| | | return insert_script |
| | | |
| | | columns = [ |
| | | self.escape_column_name(column.strip()) |
| | | for column in match.group(2).split(",") |
| | | ] |
| | | return f"{match.group(1)}{', '.join(columns)}{match.group(3)}" |
| | | |
| | | @staticmethod |
| | | def inserts(table_name: str, script_content: str) -> Generator: |
| | | PREFIX = f"INSERT INTO `{table_name}`" |
| | |
| | | Generator[str]: create index 语句 |
| | | """ |
| | | |
| | | def generate_columns(columns): |
| | | keys = [ |
| | | f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}" |
| | | for col in columns[0] |
| | | ] |
| | | return ", ".join(keys) |
| | | |
| | | for no, index in enumerate(ddl["index"], 1): |
| | | columns = generate_columns(index["columns"]) |
| | | for no, index in enumerate(ddl.get("index", []), 1): |
| | | columns = ", ".join(Convertor.index_columns(index.get("columns", []))) |
| | | if not columns: |
| | | continue |
| | | table_name = ddl["table_name"].lower() |
| | | yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" |
| | | |
| | | @staticmethod |
| | | def index_columns(columns) -> list: |
| | | """兼容 simple-ddl-parser 不同版本的索引列结构。""" |
| | | |
| | | keys = [] |
| | | |
| | | def append(name, order="ASC"): |
| | | if not name: |
| | | return |
| | | column_name = str(name).strip("`").lower() |
| | | column_order = str(order or "ASC").upper() |
| | | if column_order == "DESC": |
| | | keys.append(f"{column_name} desc") |
| | | else: |
| | | keys.append(column_name) |
| | | |
| | | def visit(value): |
| | | # 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]] |
| | | if isinstance(value, (list, tuple)): |
| | | for item in value: |
| | | visit(item) |
| | | return |
| | | if isinstance(value, dict): |
| | | name = value.get("name") |
| | | if isinstance(name, (dict, list, tuple)): |
| | | visit(name) |
| | | return |
| | | append(name, value.get("order", "ASC")) |
| | | return |
| | | # 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。 |
| | | if isinstance(value, str): |
| | | token = value.strip("`") |
| | | order = token.upper() |
| | | if order in ("ASC", "DESC"): |
| | | if order == "DESC" and keys and not keys[-1].endswith(" desc"): |
| | | keys[-1] = f"{keys[-1]} desc" |
| | | return |
| | | append(token) |
| | | |
| | | visit(columns) |
| | | return keys |
| | | |
| | | @staticmethod |
| | | def unique_index(ddl: Dict) -> Generator: |
| | |
| | | for uk in uk_list: |
| | | table_name = ddl["table_name"] |
| | | uk_name = uk["constraint_name"] |
| | | uk_columns = uk["columns"] |
| | | uk_columns = Convertor.index_columns(uk["columns"]) |
| | | if not uk_columns: |
| | | continue |
| | | yield table_name, uk_name, uk_columns |
| | | |
| | | @staticmethod |
| | |
| | | ) |
| | | nullable = "NULL" if col["nullable"] else "NOT NULL" |
| | | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| | | return f"{name} {full_type} {nullable} {default}" |
| | | return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" |
| | | |
| | | table_name = ddl["table_name"].lower() |
| | | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| | |
| | | for column in table_ddl["columns"]: |
| | | table_comment = column["comment"] |
| | | script += ( |
| | | f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" |
| | | f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" |
| | | + "\n" |
| | | ) |
| | | |
| | |
| | | """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" |
| | | |
| | | inserts = list(Convertor.inserts(table_name, self.content)) |
| | | inserts = [self.escape_insert_columns(s) for s in inserts] |
| | | # 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \,\' -> '' |
| | | inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts] |
| | | ## 生成 insert 脚本 |
| | |
| | | |
| | | |
| | | class OracleConvertor(Convertor): |
| | | reserved_column_names = {"level", "size"} |
| | | |
| | | def __init__(self, src): |
| | | super().__init__(src, "Oracle") |
| | | |
| | |
| | | # Oracle的 INSERT '' 不能通过NOT NULL校验,因此对文字类型字段覆写为 NULL |
| | | nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable |
| | | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| | | # Oracle 中 size 不能作为字段名 |
| | | field_name = '"size"' if name == "size" else name |
| | | # Oracle DEFAULT 定义在 NULLABLE 之前 |
| | | return f"{field_name} {full_type} {default} {nullable}" |
| | | return f"{self.escape_column_name(name)} {full_type} {default} {nullable}" |
| | | |
| | | table_name = ddl["table_name"].lower() |
| | | columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] |
| | |
| | | for column in table_ddl["columns"]: |
| | | table_comment = column["comment"] |
| | | script += ( |
| | | f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';" |
| | | f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" |
| | | + "\n" |
| | | ) |
| | | |
| | |
| | | """拷贝 INSERT 语句""" |
| | | inserts = [] |
| | | for insert_script in Convertor.inserts(table_name, self.content): |
| | | insert_script = self.escape_insert_columns(insert_script) |
| | | # 对日期数据添加 TO_DATE 转换 |
| | | insert_script = re.sub( |
| | | r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", |
| | |
| | | type = type.lower() |
| | | |
| | | if type == "varchar": |
| | | return f"varchar({size})" |
| | | # MySQL varchar(n) is character-oriented. DM8 may treat varchar(n) |
| | | # as bytes, so use explicit CHAR semantics for generated scripts. |
| | | return f"varchar({size} char)" |
| | | if type in ("int", "int unsigned"): |
| | | return "int" |
| | | if type in ("bigint", "bigint unsigned"): |
| | |
| | | |
| | | |
| | | class KingbaseConvertor(PostgreSQLConvertor): |
| | | reserved_column_names = {"level"} |
| | | |
| | | def __init__(self, src): |
| | | super().__init__(src) |
| | | self.db_type = "Kingbase" |
| | |
| | | if full_type == "text": |
| | | nullable = "NULL" |
| | | default = f"DEFAULT {col['default']}" if col["default"] is not None else "" |
| | | return f"{name} {full_type} {nullable} {default}" |
| | | return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" |
| | | |
| | | table_name = ddl["table_name"].lower() |
| | | columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] |
| | |
| | | |
| | | |
| | | class OpengaussConvertor(KingbaseConvertor): |
| | | reserved_column_names = set() |
| | | |
| | | def __init__(self, src): |
| | | super().__init__(src) |
| | | self.db_type = "OpenGauss" |
| | | |
| | | |
| | | class HighGoConvertor(PostgreSQLConvertor): |
| | | def __init__(self, src): |
| | | super().__init__(src) |
| | | self.db_type = "HighGo" |
| | | |
| | | |
| | | def main(): |
| | |
| | | "type", |
| | | type=str, |
| | | help="目标数据库类型", |
| | | choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss"], |
| | | choices=["postgres", "oracle", "sqlserver", "dm8", "kingbase", "opengauss", "highgo"], |
| | | ) |
| | | parser.add_argument( |
| | | "path", |
| | | type=str, |
| | | help="源数据库脚本路径", |
| | | nargs="?", |
| | | default="../mysql/ruoyi-vue-pro.sql" |
| | | ) |
| | | args = parser.parse_args() |
| | |
| | | convertor = KingbaseConvertor(sql_file) |
| | | elif args.type == "opengauss": |
| | | convertor = OpengaussConvertor(sql_file) |
| | | elif args.type == "highgo": |
| | | convertor = HighGoConvertor(sql_file) |
| | | else: |
| | | raise NotImplementedError(f"不支持目标数据库类型: {args.type}") |
| | | |