#6711·druid

[BUG] ODPS 解析连续 INNER JOIN 时将 INNER 错误识别为别名

Author: cymoftCreated Aug 30, 2026Updated Aug 30, 2026

Database Type

MaxCompute / ODPS

Database Version

不适用(纯离线 SQL 解析和序列化,无需数据库连接)

Druid Version

1.2.28(Maven Central 官方构件 com.alibaba:druid:1.2.28)

JDK Version

OpenJDK 21.0.11

Error SQL

sql
SELECT c.id
FROM context_table c
INNER JOIN loan_table l ON c.id = l.id
INNER JOIN adjust_table a ON l.id = a.id

Testcase Code

java
import com.alibaba.druid.DbType;
import com.alibaba.druid.sql.SQLUtils;
import com.alibaba.druid.sql.ast.statement.SQLJoinTableSource;
import com.alibaba.druid.sql.ast.statement.SQLSelectStatement;

public class OdpsInnerJoinTest {
    public static void main(String[] args) {
        String sql =
                "SELECT c.id " +
                "FROM context_table c " +
                "INNER JOIN loan_table l ON c.id = l.id " +
                "INNER JOIN adjust_table a ON l.id = a.id";

        SQLSelectStatement statement = (SQLSelectStatement)
                SQLUtils.parseSingleStatement(sql, DbType.odps);

        SQLJoinTableSource outer = (SQLJoinTableSource)
                statement.getSelect().getQueryBlock().getFrom();
        SQLJoinTableSource inner = (SQLJoinTableSource) outer.getLeft();

        System.out.println(SQLUtils.toOdpsString(statement));
        System.out.println("outer: type=" + outer.getJoinType()
                + ", alias=" + outer.getAlias());
        System.out.println("inner: type=" + inner.getJoinType()
                + ", alias=" + inner.getAlias());
    }
}

Stacktrace Info

没有抛出异常。解析器静默生成了语义错误的 AST。

Error Info

实际结果

序列化后 SQL 被错误改写为:

sql
SELECT c.id
FROM context_table c
INNER JOIN loan_table l ON c.id = l.id AS INNER
JOIN adjust_table a ON l.id = a.id

AST 结果:

outer: type=JOIN, alias=null
inner: type=INNER_JOIN, alias=INNER

预期结果

  1. 两个 Join 节点的类型都应为 INNER_JOIN
  2. 两个 Join 节点都不应具有 INNER 别名。
  3. 序列化结果不应凭空增加 AS INNER
  4. 对合法 ODPS SQL 执行“解析 -> 序列化”不应改变 Join 语义。

影响范围

问题不只发生在两个连续的 INNER JOIN 之间。LEFT JOINRIGHT JOINFULL JOIN 等 Join 后面紧跟显式 INNER JOIN 时,也可能把后一个 INNER 错误设置为前一个 Join 节点的别名。

由于错误结果可以再次被同一个解析器接受,使用同一解析器进行二次语法校验无法发现该语义损坏。

初步根因

SQLSelectParser.parseTableSourceRest() 在判断当前 Token 是下一个 Join 的边界还是表别名时,对 LEFTRIGHTFULL 做了向前查看,但没有对 INNER 做同等处理:

https://github.com/alibaba/druid/blob/fa8dc9912637a2f729eef9f55356621fec18d40e/core/src/main/java/com/alibaba/druid/sql/parser/SQLSelectParser.java#L1616-L1662

随后回退到别名解析,而 SQLParser.alias() / tableAlias() 允许把 Token.INNER 作为别名:

https://github.com/alibaba/druid/blob/fa8dc9912637a2f729eef9f55356621fec18d40e/core/src/main/java/com/alibaba/druid/sql/parser/SQLParser.java#L671-L810

因此,后一个 INNER JOIN 中的 INNER 被前一个表源或 Join 节点消费为别名,只剩下 JOIN 被继续解析为普通 Join。

建议回归用例

  • INNER JOIN 后跟 INNER JOIN
  • LEFT/RIGHT/FULL JOIN 后跟 INNER JOIN
  • 三个连续的 INNER JOIN
  • 验证“解析 -> 序列化 -> 再解析”前后的 Join 类型及别名保持一致
  • 验证序列化结果不会引入 AS INNER

该问题已使用 Maven Central 官方 com.alibaba:druid:1.2.28 稳定复现。检查当前 1.2.29-SNAPSHOT 的 master 源码(commit fa8dc9912637a2f729eef9f55356621fec18d40e),相关解析逻辑仍然存在。