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
预期结果
- 两个 Join 节点的类型都应为
INNER_JOIN。
- 两个 Join 节点都不应具有
INNER 别名。
- 序列化结果不应凭空增加
AS INNER。
- 对合法 ODPS SQL 执行“解析 -> 序列化”不应改变 Join 语义。
影响范围
问题不只发生在两个连续的 INNER JOIN 之间。LEFT JOIN、RIGHT JOIN、FULL JOIN 等 Join 后面紧跟显式 INNER JOIN 时,也可能把后一个 INNER 错误设置为前一个 Join 节点的别名。
由于错误结果可以再次被同一个解析器接受,使用同一解析器进行二次语法校验无法发现该语义损坏。
初步根因
SQLSelectParser.parseTableSourceRest() 在判断当前 Token 是下一个 Join 的边界还是表别名时,对 LEFT、RIGHT、FULL 做了向前查看,但没有对 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),相关解析逻辑仍然存在。
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
Testcase Code
Stacktrace Info
没有抛出异常。解析器静默生成了语义错误的 AST。
Error Info
实际结果
序列化后 SQL 被错误改写为:
AST 结果:
预期结果
INNER_JOIN。INNER别名。AS INNER。影响范围
问题不只发生在两个连续的
INNER JOIN之间。LEFT JOIN、RIGHT JOIN、FULL JOIN等 Join 后面紧跟显式INNER JOIN时,也可能把后一个INNER错误设置为前一个 Join 节点的别名。由于错误结果可以再次被同一个解析器接受,使用同一解析器进行二次语法校验无法发现该语义损坏。
初步根因
SQLSelectParser.parseTableSourceRest()在判断当前 Token 是下一个 Join 的边界还是表别名时,对LEFT、RIGHT、FULL做了向前查看,但没有对INNER做同等处理:druid/core/src/main/java/com/alibaba/druid/sql/parser/SQLSelectParser.java
Lines 1616 to 1662 in fa8dc99
随后回退到别名解析,而
SQLParser.alias()/tableAlias()允许把Token.INNER作为别名:druid/core/src/main/java/com/alibaba/druid/sql/parser/SQLParser.java
Lines 671 to 810 in fa8dc99
因此,后一个
INNER JOIN中的INNER被前一个表源或 Join 节点消费为别名,只剩下JOIN被继续解析为普通 Join。建议回归用例
INNER JOIN后跟INNER JOINLEFT/RIGHT/FULL JOIN后跟INNER JOININNER JOINAS INNER该问题已使用 Maven Central 官方
com.alibaba:druid:1.2.28稳定复现。检查当前1.2.29-SNAPSHOT的 master 源码(commitfa8dc9912637a2f729eef9f55356621fec18d40e),相关解析逻辑仍然存在。