Skip to content

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

Description

@cymoft

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

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

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 被错误改写为:

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 做同等处理:

if (tableSource.getAlias() == null || tableSource.getAlias().length() == 0) {
Token token = lexer.token;
long hash;
switch (token) {
case LEFT:
case RIGHT:
case FULL: {
Lexer.SavePoint mark = lexer.mark();
String strVal = lexer.stringVal();
lexer.nextToken();
if (lexer.token == Token.OUTER
|| lexer.token == Token.JOIN
|| lexer.identifierEquals(FnvHash.Constants.ANTI)
|| lexer.identifierEquals(FnvHash.Constants.ARRAY)
|| lexer.identifierEquals(FnvHash.Constants.SEMI)) {
lexer.reset(mark);
} else {
tableSource.setAlias(strVal);
}
}
break;
case OUTER:
break;
default:
if (identifierEquals("ARRAY")) {
Lexer.SavePoint mark = lexer.mark();
String strVal = lexer.stringVal();
lexer.nextToken();
if (lexer.token == Token.JOIN) {
lexer.reset(mark);
} else {
tableSource.setAlias(strVal);
}
break;
}
if (identifierEquals("PIVOT") || identifierEquals("UNPIVOT")) {
parsePivot(tableSource);
} else if (!(token == Token.IDENTIFIER
&& ((hash = lexer.hashLCase()) == FnvHash.Constants.STRAIGHT_JOIN
|| hash == FnvHash.Constants.CROSS))) {
boolean must = false;
if (lexer.token == Token.AS) {
lexer.nextToken();
must = true;
}
String alias = tableAlias(must);
if (alias != null) {

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

protected String alias() {
String alias = null;
if (lexer.token == Token.LITERAL_ALIAS) {
alias = lexer.stringVal();
lexer.nextToken();
} else if (lexer.token == Token.IDENTIFIER) {
alias = lexer.stringVal();
lexer.nextToken();
} else if (lexer.token == Token.LITERAL_CHARS) {
alias = "'" + lexer.stringVal() + "'";
lexer.nextToken();
} else if (lexer.token == Token.LITERAL_FLOAT && dialectFeatureEnabled(AliasLiteralFloat)) {
String numStr = lexer.numberString();
lexer.nextToken();
if (lexer.token == Token.IDENTIFIER) {
numStr += lexer.stringVal();
lexer.nextToken();
}
return numStr;
} else {
switch (lexer.token) {
case KEY:
case INDEX:
case CASE:
// case MODEL:
case PCTFREE:
case INITRANS:
case MAXTRANS:
case SEGMENT:
case CREATION:
case IMMEDIATE:
case DEFERRED:
case STORAGE:
case NEXT:
case MINEXTENTS:
case MAXEXTENTS:
case MAXSIZE:
case PCTINCREASE:
case FLASH_CACHE:
case CELL_FLASH_CACHE:
case NONE:
case LOB:
case STORE:
case ROW:
case CHUNK:
case CACHE:
case NOCACHE:
case LOGGING:
case NOCOMPRESS:
case KEEP_DUPLICATES:
case EXCEPTIONS:
case PURGE:
case INITIALLY:
case END:
case COMMENT:
case ENABLE:
case DISABLE:
case SEQUENCE:
case USER:
case ANALYZE:
case OPTIMIZE:
case GRANT:
case REVOKE:
case FULL:
case TO:
case NEW:
case INTERVAL:
case LOCK:
case LIMIT:
case IDENTIFIED:
case PASSWORD:
case BINARY:
case WINDOW:
case OFFSET:
case SHARE:
case START:
case CONNECT:
case MATCHED:
case ERRORS:
case REJECT:
case UNLIMITED:
case BEGIN:
case EXCLUSIVE:
case MODE:
case ADVISE:
case TYPE:
case CLOSE:
case OPEN:
case ANY:
case CAST:
case COMPUTE:
case ESCAPE:
case INTERSECT:
case MERGE:
case MINUS:
case SOME:
case TRUNCATE:
case UNTIL:
case VIEW:
case FUNCTION:
case DESC:
case KILL:
case SHOW:
case NULL:
case ALL:
case CONSTRAINT:
case INNER:
case LEFT:
case RIGHT:
case VALUES:
case SCHEMA:
case PARTITION:
case UPDATE:
case DO:
case LOOP:
case REPEAT:
case DEFAULT:
case LIKE:
case IS:
case UNIQUE:
case CHECK:
case INOUT:
case DECLARE:
case TABLE:
case TRIGGER:
case IN:
case OUT:
case BY:
case EXCEPT:
case TABLESPACE:
case CREATE:
case DELETE:
case PRIMARY:
case FOREIGN:
case REFERENCES:
case INTO:
case USE:
case LEAVE:
case DISTRIBUTE:
case AS:

因此,后一个 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),相关解析逻辑仍然存在。

Activity

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