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