Skip to content

这个sql执行时出现了错误 #9

Closed

Description

@xlorne

update biz_pbm_organization set short_name_tair=replace(short_name_tair,?,?) where system_code like (?||'%') and short_name_tair like (?||'%') and id<>? and sys_deleted=0

Activity

  1. xlorne commented on Sep 22, 2026

    @xlorne
    ContributorAuthor

    dbstream-driver 缺陷报告:UPDATE 的 SET 子句含函数调用时生成非法 SQL

    项 值
    缺陷组件 com.codingapi.dbstream:dbstream-driver
    缺陷版本 1.0.19(当前最新 release;1.0.19-SNAPSHOT 同样未修)
    数据库 达梦 DM8 8.1.3.26
    JDBC 驱动 com.dameng:DmJdbcDriver18:8.1.3.140
    触发方 Spring Data JPA @Modifying @Query 批量 UPDATE,SET 值为函数调用
    责任判定 dbstream 生成的 SQL 非法;数据库只是正确拒绝了一条语法非法的 SQL
    发现日期 2026-09-22

    1. 现象

    单位管理更新「系统简称」时接口报错,日志把错误记在一条看起来完全正常的业务 UPDATE 上:

    WARN  org.hibernate.engine.jdbc.spi.SqlExceptionHelper - SQL Error: -2007, SQLState: 42000
    ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper - 第 1 行, 第 26 列[)]附近出现错误:
    语法分析出错
    
    org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL
    [update biz_pbm_organization set short_name_tair=replace(short_name_tair,?,?)
     where system_code like (?||'%') and short_name_tair like (?||'%') and id<>? and sys_deleted=0]
    [第 1 行, 第 26 列[)]附近出现错误: 语法分析出错]
    

    报错链路:

    PbmOrganizationService.updateOrganization
      → PbmOrganizationEntityRepository.updateChildrenShortNameTair   (JPQL 批量 UPDATE)
        → Hibernate executeUpdate
          → MyDriver( dbstream ConnectionProxy → 数据权限 ConnectionProxy → DmDriver )
            → dbstream 前镜像查询报语法错
    

    2. 责任代码

    2.1 UpdateSQLParser.getColumnValues() —— 根因

    取 SET 子句(正则 (?i)\bSET\b\s+(.*?)(?=\bWHERE\b|$))后直接 split(","),再对每段取 split("=", 2)[0] 作列名:

    String setClause = matcher.group(1).trim();     // "short_name_tair=replace(short_name_tair,?,?)"
    String[] parts = setClause.split(",");          // ["short_name_tair=replace(short_name_tair", "?", "?)"]
    for (String part : parts) {
        String[] kv = part.split("=", 2);
        if (kv.length > 0) {
            String name = kv[0].trim();
            int idx = name.lastIndexOf('.');
            if (idx > 0) name = name.substring(idx + 1);
            name = SQLUtils.stripQuotes(name);
            if (!name.isEmpty()) list.add(name);     // 产出 "?" 与 "?)" 两个伪列名
        }
    }

    它假设「SET 子句里逗号只用于分隔列」,但 REPLACE(col, ?, ?) / CONCAT(a, b) / SUBSTR(s, 1, 2) 这类函数调用的参数之间也有逗号,于是被当成列分隔符。

    2.2 UpdateDBEventParser.latestSQL() / getPrimaryKeyStringValue() —— 拼出非法 SQL

    用 2.1 的列名拼「前镜像」(旧值)查询:

    SELECT <受影响列>, <主键列> FROM <表> WHERE <主键> IN (<值>) AND 1=1
    

    latestSQL() 里 SET 列名与主键列名用 String.join(",", ...) 拼接,UpdateSQLParser 的脏列名就这样被带进 SQL 文本。

    3. 最小复现(无需数据库,直接调 dbstream 解析器)

    import com.codingapi.dbstream.parser.UpdateSQLParser;
    // dbstream-driver-1.0.19.jar 单独在 classpath 即可
    
    UpdateSQLParser p = new UpdateSQLParser(
        "update biz_pbm_organization set short_name_tair=replace(short_name_tair,?,?) " +
        "where system_code like (?||'%') and short_name_tair like (?||'%') and id<>? and sys_deleted=0");
    System.out.println(p.getColumnValues());
    // 实际输出: [short_name_tair, ?, ?)]
    // 期望输出: [short_name_tair]

    拼出的前镜像查询(非法):

    SELECT short_name_tair,?,?),ID FROM BIZ_PBM_ORGANIZATION WHERE ID IN (...) AND 1=1

    对照:SET 里不含函数时解析正常(业务里日志成功的那批前镜像查询就是这种形态):

    new UpdateSQLParser("update biz_pbm_organization set short_name=?, short_name_tair=?, sys_update_time=?, " +
        "sys_updater_id=?, sys_version=? where id=? and sys_version=?")
    // 输出: [short_name, short_name_tair, sys_update_time, sys_updater_id, sys_version]   ✅

    4. 数据库侧实测

    在同一个达梦库上逐条执行(DmJdbcDriver18 直连):

    # SQL 结果
    1 SELECT short_name_tair,?,?),ID FROM BIZ_PBM_ORGANIZATION WHERE ID IN (784812337803333) AND 1=1(dbstream 产物) ❌ SQLState 42000 / -2007,第 1 行, 第 26 列[)]附近出现错误: 语法分析出错
    2 那条业务 UPDATE 原样(replace(...) + `like (?
    3 SELECT short_name_tair,ID FROM BIZ_PBM_ORGANIZATION WHERE ID IN (784812337803333) AND 1=1(黄金对照) ✅

    结论:达梦支持 REPLACE、支持 ?||'%'、支持 SET 里的参数占位符;它报错的唯一原因是第 1 条 SQL 本身非法。
    第 1 条的报错文案与线上日志逐字一致,可确认线上失败的就是这条 dbstream 内部查询。

    另:IN () 空列表同样非法(见第 6 节)。

    5. 影响:错误归属误导,排查成本高

    前镜像查询的 SQLException 在 PreparedStatementProxy.executeUpdate() 内部(SQLRunningContext.before())抛出并向上传播,
    调用方 Hibernate 看到的是自己的 UPDATE 失败,于是打印 JDBC exception executing SQL [<业务 UPDATE 原文>] [<dbstream SQL 的报错>]。
    一条合法 SQL 顶着另一条非法 SQL 的报错,且 dbstream 内部 SQL 不出现在任何日志里(它不经过数据权限拦截器的 SQL 日志),
    定位难度很大。

    建议:前镜像查询失败时包一层,明确标注「dbstream 前镜像查询失败」并带上内部 SQL 原文,避免误导使用方。

    6. 顺带发现的同类问题

    1. 主键列表为空时拼出 WHERE ID IN ():latestSQL() 中 getPrimaryKeyStringValue() 返回空集合时仍会拼出 IN (),
      实测达梦报 第 1 行, 第 49 列[)]附近出现错误。建议 prepareList 为空时直接跳过前镜像查询与事件生成。
    2. DeleteDBEventParser.getWhereSQL() 用正则 (?i)\bWHERE\b\s+(.*)$ 直接截取 WHERE 文本,建议一并自查同类边界。

    7. 修复建议

    UpdateSQLParser.getColumnValues() 不要用 split(","),按括号深度 + 引号状态切分 SET 项(仅在深度为 0 且不在引号内时才切),
    或直接改用 SQL 解析器(dbstream 生态里已有 net.sf.jsqlparser,解析 Update 语句取 UpdateSet 项最稳)。

    8. 复现脚本

    文件 用途
    /tmp/dmprobe/DbstreamProof.java 第 3 节:调 dbstream 解析器打印列名
    /tmp/dmprobe/DmProbe2.java 第 4 节:达梦实测逐字报错
    /tmp/dmprobe/DmProbe3.java 替代写法实测(无逗号的 SUBSTRING(x FROM n) 等)
  2. added 2 commits that reference this issue on Sep 22, 2026
    683ca0e
    d95f453
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions