Commit f4903d81 authored by Data Governance Dev's avatar Data Governance Dev

fix(backend): Oracle 自定义规则 ORA-01036 + ORA-00933

两个 Oracle 专属问题,连续两次跑自定义规则暴露:

1) ORA-01036: illegal variable name/number
   - 原因:sql_builder._placeholder 旧版 Oracle 路径输出 '?',但 python-oracledb
     不支持 qmark 占位符(thin 模式不会自动转 :N,必须显式给 numbered)
   - 修复:Bind 加 next_idx;Oracle 路径按递增编号输出 :1, :2, ...,与
     bind.params 顺序一一对应

2) ORA-00933: SQL command not properly ended
   - 原因:step9_custom_rules 无条件拼 'LIMIT 200',Oracle 11g 不支持 LIMIT
     (12c 引入 FETCH FIRST;11g 只能用 WHERE ROWNUM <= N)
   - 修复:截断子句按 db_type 分支,Oracle 走 WHERE ROWNUM <= 200

中间 bug:Oracle 分支漏写 WHERE 关键字(FROM "tbl" "col" LIKE :1 ...),
静态验证脚本(三个方言各跑一遍)暴露后补上 WHERE。

静态验证 SQL:
  MySQL  : SELECT `COL` FROM `TBL` WHERE `COL` LIKE %s ESCAPE '!' LIMIT 200
  达梦   : SELECT "COL" FROM "TBL" WHERE "COL" LIKE ?  ESCAPE '!' LIMIT 200
  Oracle : SELECT "COL" FROM "TBL" WHERE "COL" LIKE :1 ESCAPE '!' AND ROWNUM <= 200

WORKLOG 待用户在 Oracle 端跑自定义规则验证后追加(不阻塞提交)
parent ce27e205
...@@ -15,8 +15,14 @@ ...@@ -15,8 +15,14 @@
传给 db.fetchall(sql, params) 完成绑定。 传给 db.fetchall(sql, params) 完成绑定。
跨 DB 占位符: 跨 DB 占位符:
MySQL → %s (pymysql pyformat) MySQL → %s (pymysql pyformat)
达梦 / Oracle → ? (dmPython qmark;oracledb thin 自动转 :1) 达梦 → ? (dmPython qmark)
Oracle → :1, :2, ... (oracledb numbered binding)
注意:oracledb **不支持** qmark (?) 占位符,必须用 numbered (:1, :2) 或 named (:name)。
本模块对 Oracle 走 numbered:每个 leaf 取一次 bind.next_idx 后 +1,保证 :N 严格对应
params 列表的第 N-1 个值。早期注释里写的「oracledb thin 自动转 :1」是错的,
2026-08-13 跑 Oracle 自定义规则时 ORA-01036: illegal variable name/number 暴露。
占位符在每条 leaf 里**单独**取一次(db_type 全局固定),保证整条 SQL 用同一种风格。 占位符在每条 leaf 里**单独**取一次(db_type 全局固定),保证整条 SQL 用同一种风格。
""" """
...@@ -32,13 +38,30 @@ from .rule_types import comparator_to_sql_op, is_like_comparator ...@@ -32,13 +38,30 @@ from .rule_types import comparator_to_sql_op, is_like_comparator
# ── 参数绑定容器 ───────────────────────────────────────── # ── 参数绑定容器 ─────────────────────────────────────────
@dataclass @dataclass
class Bind: class Bind:
"""递归过程中累积的绑定参数;调用方最后拿 tuple(bind.params) 给 DB 驱动。""" """递归过程中累积的绑定参数;调用方最后拿 tuple(bind.params) 给 DB 驱动。
next_idx 只在 Oracle 路径下用,每发一个 numbered 占位符(:1, :2, ...)+1,
保证占位符编号与 params 列表顺序一一对应。
"""
params: list = field(default_factory=list) params: list = field(default_factory=list)
next_idx: int = 1
def _placeholder(db_type: str) -> str: def _placeholder(db_type: str, bind: Bind) -> str:
"""跨方言占位符(每个 leaf 调一次,避免组拼出来混用)。""" """跨方言占位符(每个 leaf 调一次,避免组拼出来混用)。
return "%s" if (db_type or "").lower() == "mysql" else "?"
Oracle 走 numbered(:1, :2, ...),每发一次 +1。
MySQL / 达梦 不需要编号(%s / ? 按位置对应)。
"""
db = (db_type or "").lower()
if db == "mysql":
return "%s"
if db == "oracle":
idx = bind.next_idx
bind.next_idx += 1
return f":{idx}"
# dameng: dmPython 用 qmark 占位符
return "?"
# ── 主入口 ──────────────────────────────────────────────── # ── 主入口 ────────────────────────────────────────────────
...@@ -93,7 +116,7 @@ def build_where( ...@@ -93,7 +116,7 @@ def build_where(
pattern = f"%{escape_like(str(val))}%" pattern = f"%{escape_like(str(val))}%"
bind.params.append(pattern) bind.params.append(pattern)
sql_op = "LIKE" if cmp == "contains" else "NOT LIKE" sql_op = "LIKE" if cmp == "contains" else "NOT LIKE"
return f"{column_quoted} {sql_op} {_placeholder(db_type)} ESCAPE '{ESCAPE_CHAR}'" return f"{column_quoted} {sql_op} {_placeholder(db_type, bind)} ESCAPE '{ESCAPE_CHAR}'"
# 算术系(> / < / = / date_before / date_after) # 算术系(> / < / = / date_before / date_after)
sql_op = comparator_to_sql_op(cmp) sql_op = comparator_to_sql_op(cmp)
...@@ -112,7 +135,7 @@ def build_where( ...@@ -112,7 +135,7 @@ def build_where(
else: else:
bind_val = str(val) if val is not None else "" bind_val = str(val) if val is not None else ""
bind.params.append(bind_val) bind.params.append(bind_val)
return f"{column_quoted} {sql_op} {_placeholder(db_type)}" return f"{column_quoted} {sql_op} {_placeholder(db_type, bind)}"
# ── 顶层便捷函数 ────────────────────────────────────────── # ── 顶层便捷函数 ──────────────────────────────────────────
......
...@@ -189,12 +189,23 @@ def run_step_custom_value_check( ...@@ -189,12 +189,23 @@ def run_step_custom_value_check(
skipped_no_valid_leaf += 1 skipped_no_valid_leaf += 1
continue continue
sql = ( # 截断子句按 db_type 分支:MySQL/达梦走 LIMIT;Oracle 11g 不支持 LIMIT/FETCH,
f"SELECT {col_quoted} " # 用 WHERE ROWNUM <= N(兼容 11.2.0.4;12c+ 也仍支持 ROWNUM 写法)。
f"FROM {quote_ident(table_name, cfg.db_type)} " # 2026-08-13:Oracle 上跑自定义规则报 ORA-00933 暴露此差异。
f"WHERE {where_clause} " # where_clause 已由 L187 保证非空,所以可直接拼 AND ROWNUM <= N。
f"LIMIT {_LIMIT}" if cfg.db_type == "oracle":
) sql = (
f"SELECT {col_quoted} "
f"FROM {quote_ident(table_name, cfg.db_type)} "
f"WHERE {where_clause} AND ROWNUM <= {_LIMIT}"
)
else:
sql = (
f"SELECT {col_quoted} "
f"FROM {quote_ident(table_name, cfg.db_type)} "
f"WHERE {where_clause} "
f"LIMIT {_LIMIT}"
)
try: try:
rows = db.fetchall(sql, tuple(params)) rows = db.fetchall(sql, tuple(params))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment