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
......@@ -16,7 +16,13 @@
跨 DB 占位符:
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 用同一种风格。
"""
......@@ -32,13 +38,30 @@ from .rule_types import comparator_to_sql_op, is_like_comparator
# ── 参数绑定容器 ─────────────────────────────────────────
@dataclass
class Bind:
"""递归过程中累积的绑定参数;调用方最后拿 tuple(bind.params) 给 DB 驱动。"""
"""递归过程中累积的绑定参数;调用方最后拿 tuple(bind.params) 给 DB 驱动。
next_idx 只在 Oracle 路径下用,每发一个 numbered 占位符(:1, :2, ...)+1,
保证占位符编号与 params 列表顺序一一对应。
"""
params: list = field(default_factory=list)
next_idx: int = 1
def _placeholder(db_type: str) -> str:
"""跨方言占位符(每个 leaf 调一次,避免组拼出来混用)。"""
return "%s" if (db_type or "").lower() == "mysql" else "?"
def _placeholder(db_type: str, bind: Bind) -> str:
"""跨方言占位符(每个 leaf 调一次,避免组拼出来混用)。
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(
pattern = f"%{escape_like(str(val))}%"
bind.params.append(pattern)
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)
sql_op = comparator_to_sql_op(cmp)
......@@ -112,7 +135,7 @@ def build_where(
else:
bind_val = str(val) if val is not None else ""
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,6 +189,17 @@ def run_step_custom_value_check(
skipped_no_valid_leaf += 1
continue
# 截断子句按 db_type 分支:MySQL/达梦走 LIMIT;Oracle 11g 不支持 LIMIT/FETCH,
# 用 WHERE ROWNUM <= N(兼容 11.2.0.4;12c+ 也仍支持 ROWNUM 写法)。
# 2026-08-13:Oracle 上跑自定义规则报 ORA-00933 暴露此差异。
# where_clause 已由 L187 保证非空,所以可直接拼 AND ROWNUM <= N。
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)} "
......
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