Commit af9b6253b12c5381378412319cf423ffa6d42530

Authored by YunaiV
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"