mysql转达梦-打包操作
目的:现已经写好了一个拦截器DmSqlRewriteInterceptor,可以实现将mysql的sql语句转换为达梦的sql语句。打包的目的是希望,在将mysql切换为达梦时,只需要导入我这个包以及修改数据库连接的配置,就能直接实现mysql数据库到达梦数据库的切换
打包项目的名称:dm-sql-rewrite-starter
项目目录的结构:
dm-sql-rewrite-starter/
├─ src/main/java/com/yourcompany/dm/
│ ├─ interceptor/
│ │ └─ DmSqlRewriteInterceptor.java // 你的拦截器(稍作构造器调整以接收props)
│ ├─ autoconfig/
│ │ └─ DmSqlRewriteAutoConfiguration.java // 自动装配类(关键)
│ └─ properties/
│ └─ DmSqlRewriteProperties.java // 可配置项(prefix=dm.sql.rewrite)
├─ src/main/resources/
│ ├─ META-INF/spring.factories
│ └─ spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
└─ pom.xml
pom.xml文件:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zdsc</groupId>
<artifactId>dm-sql-rewrite-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>dm-sql-rewrite-starter</name>
<description>Starter for converting MySQL applications to Dameng Database</description>
<url>http://www.zdsc.com</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.5.4</spring-boot.version>
<mybatis.version>3.5.7</mybatis.version>
<jsqlparser.version>4.0</jsqlparser.version>
</properties>
<dependencies>
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<!-- Spring Boot AutoConfiguration -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>${spring-boot.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.3</version>
</dependency>
<!-- Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
<optional>true</optional>
</dependency>
<!-- MyBatis Core -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
<scope>provided</scope>
</dependency>
<!-- JSqlParser for SQL rewriting -->
<!-- 注意,这里一定不能写<scope>provided</scope> -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
<scope>provided</scope>
</dependency>
<!--达梦数据驱动-->
<!-- 注意,这里一定不能写<scope>provided</scope> 因为达梦的驱动包是自己手动导入的,所以打包的时候,必须也得带上DmJdbcDriver的jar包-->
<dependency>
<groupId>com.dm</groupId>
<artifactId>DmJdbcDriver</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<!-- 让生成的主构件就是 shaded 后的 jar(而不是额外的 -shaded.jar) -->
<shadedArtifactAttached>false</shadedArtifactAttached>
<!-- 生成 dependency-reduced-pom,避免使用方再拉同一个依赖形成重复 -->
<createDependencyReducedPom>true</createDependencyReducedPom>
<!-- 只把下面列出的依赖打进来,避免把整个依赖树都塞进 starter -->
<artifactSet>
<includes>
<!-- 必选:达梦驱动 -->
<include>com.dm:DmJdbcDriver</include>
<!-- 可选:如果你也想“物理打包” jsqlparser,而不是仅做传递依赖,就保留下一行 -->
<include>com.github.jsqlparser:jsqlparser</include>
</includes>
</artifactSet>
<!-- 仅对 jsqlparser 做 relocate,避免与使用方可能已有版本冲突。
注意:千万不要 relocate 达梦驱动,否则 driver-class-name 就找不到了 -->
<relocations>
<relocation>
<pattern>net.sf.jsqlparser</pattern>
<shadedPattern>com.zdsc.shaded.net.sf.jsqlparser</shadedPattern>
</relocation>
</relocations>
<!-- 合并 SPI 文件,保证 META-INF/services 可正常工作(虽说 Hikari 用类名加载,但合并更稳妥) -->
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring-boot.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Maven Jar Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
</plugin>
<!-- Spring Boot Maven Plugin (optional) -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<configuration>
<skip>true</skip> <!-- 不需要打包可执行JAR -->
</configuration>
</plugin>
</plugins>
</build>
<!-- 发布到仓库的配置 -->
<distributionManagement>
<repository>
<id>nexus-releases</id>
<url>http://your-nexus/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<url>http://your-nexus/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
</project>
DmSqlRewriteAutoConfiguration
/**
* DM数据库SQL重写拦截器的自动配置类(兼容多版本的 Configuration 插件检查)
*/
@Configuration
@ConditionalOnClass(name = "org.apache.ibatis.session.SqlSessionFactory")
@EnableConfigurationProperties(DmSqlRewriteProperties.class)
@ConditionalOnProperty(name = "dm.sql.rewrite.enabled", havingValue = "true", matchIfMissing = true)
public class DmSqlRewriteAutoConfiguration {
@Autowired
private DmSqlRewriteProperties properties;
@Bean
@ConditionalOnMissingBean
public DmSqlRewriteInterceptor dmSqlRewriteInterceptor() {
return new DmSqlRewriteInterceptor(properties.isQuoteAndToLower(), properties.isDebugLog());
}
@Bean
@ConditionalOnMissingBean
public ConfigurationCustomizer dmConfigurationCustomizer(DmSqlRewriteInterceptor interceptor) {
return configuration -> {
boolean exists = false;
try {
// 尝试多种可能的方法名(getPlugins / getInterceptors)
Method m = null;
try { m = configuration.getClass().getMethod("getPlugins"); } catch (NoSuchMethodException ignored) {}
if (m == null) {
try { m = configuration.getClass().getMethod("getInterceptors"); } catch (NoSuchMethodException ignored) {}
}
if (m != null) {
Object res = m.invoke(configuration);
if (res != null) {
if (res.getClass().isArray()) {
Object[] arr = (Object[]) res;
for (Object p : arr) {
if (p != null && p.getClass().getName().equals(interceptor.getClass().getName())) {
exists = true;
break;
}
}
} else if (res instanceof Collection) {
for (Object p : (Collection<?>) res) {
if (p != null && p.getClass().getName().equals(interceptor.getClass().getName())) {
exists = true;
break;
}
}
}
}
}
} catch (Throwable t) {
// 反射检查出错了,降级处理:不阻止注册(会在下方尝试注册)
// 可选:记录日志
// logger.warn("检查已注册 Interceptor 时出错,改为直接注册:{}", t.getMessage());
}
if (!exists) {
try {
configuration.addInterceptor((Interceptor) interceptor);
} catch (Throwable t) {
// 如果强制 addInterceptor 也失败(极少见),可以记录日志
// logger.error("将 DmSqlRewriteInterceptor 注册到 MyBatis Configuration 失败", t);
}
}
};
}
}
DmSqlRewriteProperties
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "dm.sql.rewrite")
public class DmSqlRewriteProperties {
/**
* 是否启用自动改写拦截器
*/
private boolean enabled = true;
/**
* 原来拦截器你的构造参数:是否把标识符转小写并加引号
*/
private boolean quoteAndToLower = true;
/**
* 是否打印原 SQL -> 改写后 SQL 的 debug 日志
*/
private boolean debugLog = false;
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public boolean isQuoteAndToLower() { return quoteAndToLower; }
public void setQuoteAndToLower(boolean quoteAndToLower) { this.quoteAndToLower = quoteAndToLower; }
public boolean isDebugLog() { return debugLog; }
public void setDebugLog(boolean debugLog) { this.debugLog = debugLog; }
}
DmSqlRewriteInterceptor
package com.zdsc.dm.interceptor;
import net.sf.jsqlparser.expression.*;
import net.sf.jsqlparser.expression.operators.relational.*;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.schema.Table;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* DmSqlRewriteInterceptor
* <p>
* 用途:在 StatementHandler.prepare 时,对 SQL 做简单的兼容处理 ——
* 当前版本仅支持“去掉反引号`”的操作(可配置开关 removeBackticks)。
* <p>
* 说明:为了最小化对现有逻辑的改动,保留了原先基于 JSqlParser 的方法(以便未来扩展),
* 但拦截器的实际行为现在是:当 removeBackticks=true 且数据库为达梦时,
* 在把 SQL 写回到 BoundSql 之前,对 SQL 字符串做一次稳健的反引号删除(不会删除字符串字面量内的反引号)。
*/
/**
* @Intercepts / @Signature:告诉 MyBatis:把这个拦截器织入到 StatementHandler.prepare(Connection,Integer) 调用点。
* 也就是说,当 MyBatis 在准备 JDBC PreparedStatement 时,会调用此拦截器的 intercept。
* <p>
* implements Interceptor:实现 MyBatis 的插件接口,需要实现 intercept、plugin、setProperties。
*/
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DmSqlRewriteInterceptor implements Interceptor {
//日志记录器
private static final Logger logger = LoggerFactory.getLogger(DmSqlRewriteInterceptor.class);
//sqlCache:缓存原 SQL → 改写后 SQL。避免每次请求都解析 SQL,性能关键。用 ConcurrentHashMap 线程安全。
private final ConcurrentHashMap<String, String> sqlCache = new ConcurrentHashMap<>();
// 类成员:记录别名被重命名的映射 oldAlias -> newAlias 每个线程都有自己独立的 Map
private final ThreadLocal<Map<String, String>> aliasRenameMap = ThreadLocal.withInitial(HashMap::new);
// 类成员:保留字集合(大写),可根据需要扩展/从配置加载
private static final Set<String> RESERVED_WORDS = new HashSet<>(Arrays.asList(
"USER", "GROUP", "ORDER", "SELECT", "FROM", "WHERE", "JOIN", "TABLE", "INDEX", "PRIMARY",
"VALUES", "LIMIT", "LIKE", "HAVING", "KEY", "TYPE", "DESC", "ASC", "CURRENT_DATE",
"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "GRANT", "REVOKE"
// 按需扩展或从配置加载
));
// 新增:是否移除反引号(`)
private final boolean removeBackticks;
// 是否打印改写日志(生产建议 false,开发可 true)
private final boolean debugLog;
//提供默认构造器(默认启用改写、关闭详细日志)以及可配置构造器,便于在 Spring 注册时传入不同参数
public DmSqlRewriteInterceptor() {
this(true, false);
}
public DmSqlRewriteInterceptor(boolean removeBackticks, boolean debugLog) {
this.removeBackticks = removeBackticks;
this.debugLog = debugLog;
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 取 Connection(prepare 的第一个参数)
//去JDBC Connection prepare (Connection,int) 的第一个参数就是 JDBC Connection。我们拿到它用于判断数据库类型(只对 DM 生效)。
Connection connection = (Connection) invocation.getArgs()[0];
// 2. 仅在达梦 DB 时生效
try {
if (connection == null || connection.getMetaData() == null) {
return invocation.proceed();
}
String productName = connection.getMetaData().getDatabaseProductName();
logger.info("当前数据库连接的产品是: {}", productName);
if (productName == null || !productName.toLowerCase().contains("dm")) {
return invocation.proceed();
}
} catch (Exception e) {
logger.debug("无法获取数据库产品名称,跳过 SQL 重写.", e);
return invocation.proceed();
}
// MyBatis 在运行时会把 StatementHandler 包多层代理(插件链),因此需要“解包”(unwrap)才能安全访问内部字段。
Object target = invocation.getTarget();
//SystemMetaObject.forObject(target):把对象包装成 MetaObject,可以通过字符串路径读取内部属性(例如 delegate.boundSql)。
MetaObject metaObject = SystemMetaObject.forObject(target);
/**
* 先连续 hasGetter("h")、hasGetter("target") 的解包循环,常见于 JDK 动态代理(h),和 MyBatis 的 Plugin(target)。
* 保证我们最终能访问到真正的 StatementHandler 内部 delegate 对象。
*/
// 常见解包:JDK 动态代理 handler 字段为 "h"
while (metaObject.hasGetter("h")) {
Object h = metaObject.getValue("h");
metaObject = SystemMetaObject.forObject(h);
}
// MyBatis Plugin 包装时,内部对象通常叫 "target"
while (metaObject.hasGetter("target")) {
Object inner = metaObject.getValue("target");
metaObject = SystemMetaObject.forObject(inner);
}
BoundSql boundSql;
try {
boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
} catch (Exception ex) {
// 兜底:若无法通过 metaObject 取到,尝试直接 cast
try {
//如果以上方法都失败,尝试 target instanceof StatementHandler 强转取 getBoundSql()(另一个兜底路径)。
//要尽量通用地获取 BoundSql,避免因为代理层次不同而抛异常(这是之前遇到“no getter for delegate”的原因)。
if (target instanceof StatementHandler) {
boundSql = ((StatementHandler) target).getBoundSql();
} else {
logger.debug("无法获取 BoundSql,跳过重写。", ex);
return invocation.proceed();
}
} catch (Exception ex2) {
logger.debug("无法获取 BoundSql,跳过重写。", ex2);
return invocation.proceed();
}
}
//若 SQL 为空(可能不是 DML/查询),则不处理直接执行。
String originalSql = boundSql == null ? null : boundSql.getSql();
if (originalSql == null || originalSql.trim().isEmpty()) {
return invocation.proceed();
}
logger.info("原始 SQL语句为: {}", originalSql);
// 只做去反引号(带缓存),不再使用 JSqlParser 解析或转小写等操作
String rewritten = rewriteSqlCached(originalSql);
logger.info("改造后的 SQL语句为: {}", rewritten);
logger.debug("原始的 SQL语句为: {}", originalSql);
/**
* 如果改写后 SQL 与原 SQL 不同,用 metaObject.setValue("delegate.boundSql.sql", rewritten)
* 把改写后的 SQL 写回 MyBatis 的 BoundSql 中。这样 MyBatis 后续会使用改写后的 SQL 去 prepare statement 并执行
* ,不影响参数绑定。
*/
if (!originalSql.equals(rewritten)) {
try {
metaObject.setValue("delegate.boundSql.sql", rewritten);
if (debugLog) {
logger.info("原始的sql-->改写后的sql:\n{}\n--> {}\n", originalSql, rewritten);
} else {
// 采样日志:只记录少量示例以便排错(这里简单记录)
logger.debug("重写SQL(示例): {}", rewritten.length() > 200 ? rewritten.substring(0, 200) + "..." : rewritten);
}
} catch (Exception e) {
// 写回失败不影响执行(回退)
logger.warn("设置重写的SQL失败,继续原始操作。原因: {}", e.getMessage());
//写回失败(反射或路径变化)会 catch 并回退(保证业务不被中断)。
return invocation.proceed();
}
}
return invocation.proceed();
}
/**
*
* @param target
* @return plugin:MyBatis 要求返回包装后的对象(通常直接 Plugin.wrap)。
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* setProperties:允许通过配置文件传入 properties(这里留空,可扩展为从 properties 中读取 quoteAndToLower / debugLog)。
*
* @param properties
*/
@Override
public void setProperties(Properties properties) {
// 可通过 properties 读取开关
}
/**
* 带缓存的改写(线程安全)
* 现在的实现仅做去反引号并使用缓存。
*/
private String rewriteSqlCached(String sql) {
// 若不开启 removeBackticks,直接返回原 SQL(不走解析)
if (!removeBackticks) return sql;
if (sql == null || sql.isEmpty()) return sql;
// 缓存 key 带上标识,避免未来策略变化冲突
String key = "R:" + sql;
String cached = sqlCache.get(key);
if (cached != null) return cached;
String rewritten = sql;
try {
// 使用 JSqlParser 的 AST 路径做逐节点处理(保证你后续修改函数时有位置可扩展)
rewritten = rewriteSqlWithJSqlParser(sql, removeBackticks /* 参数仅作占位: 使用 removeBackticks 字段控制实际行为 */);
} catch (Throwable t) {
logger.debug("JSqlParser解析SQL失败,返回原始SQL。原因: {}", t.getMessage());
rewritten = sql;
}
sqlCache.putIfAbsent(key, rewritten);
return sqlCache.get(key);
}
/**
* 用 JSqlParser 把传入的 SQL 字符串解析成 AST,然后针对常见 DML(SELECT/INSERT/UPDATE/DELETE)
* <p>
* 说明:此方法保留以便未来需要,但当前拦截逻辑并不调用它。
*/
private String rewriteSqlWithJSqlParser(String sql, boolean removeBackticks) throws Exception {
//新增:每条语句开始前清空映射,避免串台
clearAliasRenameMap();
// 解析 SQL 为 AST
Statement stmt = CCJSqlParserUtil.parse(sql);
// 在一次语句处理过程中复用同一个 visitor,保证 aliasRenameMap 的一致性
ColumnExpressionVisitor visitor = new ColumnExpressionVisitor(removeBackticks);
if (stmt instanceof Select) {
Select select = (Select) stmt;
SelectBody body = select.getSelectBody();
processSelectBody(body, removeBackticks);
return select.toString();
} else if (stmt instanceof Insert) {
Insert insert = (Insert) stmt;
// 处理表名
if (insert.getTable() != null) {
insert.getTable().accept(visitor);
}
// 处理列列表
if (insert.getColumns() != null) {
insert.getColumns().forEach(c -> c.accept(visitor));
}
// 处理 itemsList:支持 SubSelect、ExpressionList、MultiExpressionList(VALUES 多行)
ItemsList itemsList = insert.getItemsList();
// 插入值中可能包含 subselect,若有需要可以递归处理(JSqlParser 的 Insert 支持 getItemsList)
if (itemsList instanceof SubSelect) {
SubSelect ss = (SubSelect) itemsList;
if (ss.getSelectBody() != null) processSelectBody(ss.getSelectBody(), removeBackticks);
} else if (itemsList instanceof ExpressionList) {
ExpressionList el = (ExpressionList) itemsList;
if (el.getExpressions() != null) {
el.getExpressions().forEach(e -> {
if (e != null) e.accept(visitor);
});
}
} else if (itemsList instanceof MultiExpressionList) {
MultiExpressionList mel = (MultiExpressionList) itemsList;
mel.getExprList().forEach(exprList -> {
if (exprList.getExpressions() != null) {
exprList.getExpressions().forEach(e -> {
if (e != null) e.accept(visitor);
});
}
});
}
return insert.toString();
} else if (stmt instanceof Update) {
Update up = (Update) stmt;
// 处理表名
if (up.getTable() != null) {
up.getTable().accept(visitor);
}
// 处理要更新的列
if (up.getColumns() != null) {
up.getColumns().forEach(c -> {
if (c != null) c.accept(visitor);
});
}
// 处理 WHERE 条件
if (up.getWhere() != null) {
up.getWhere().accept(visitor);
}
return up.toString();
} else if (stmt instanceof Delete) {
Delete del = (Delete) stmt;
if (del.getTable() != null) {
del.getTable().accept(visitor);
}
if (del.getWhere() != null) {
del.getWhere().accept(visitor);
}
return del.toString();
}
// 未处理的语句类型回退原 SQL
return sql;
}
/* --------------------- 辅助 AST 处理方法 --------------------- */
/**
* 这是一个私有方法(只在当前类内使用),输入是一个 SelectBody(JSqlParser 表示 SELECT 语句主体的抽象),和一个布尔开关 removeBackticks(表示在处理节点时是否执行去除反引号的操作)。
* <p>
* 设计意图:统一入口,递归/分发处理各种 SELECT 的具体变体(普通 SELECT、UNION/INTERSECT/...、CTE/ WITH 子句等)。
*
* @param body
* @param removeBackticks
*/
private void processSelectBody(SelectBody body, boolean removeBackticks) {
if (body == null) return;
if (body instanceof PlainSelect) {
/**
* 检查 body 是否为 PlainSelect(最常见的一类:SELECT ... FROM ... [WHERE ...] [GROUP BY ...] [ORDER BY ...])。
* 通常包含 FROM、JOIN、SELECT 列表、WHERE、GROUP BY、HAVING、ORDER BY 等子结构。
*/
plainSelectHandle((PlainSelect) body, removeBackticks);
} else if (body instanceof SetOperationList) {
/**
* 处理集合操作的 SELECT,例如 UNION / UNION ALL / INTERSECT / EXCEPT 等。
*/
SetOperationList sol = (SetOperationList) body;
List<SelectBody> selects = sol.getSelects();
//嵌套语句,对每个分支递归调用 processSelectBody,以保证 UNION 的每一部分都能被同样改写。
if (selects != null) selects.forEach(sb -> processSelectBody(sb, removeBackticks));
} else if (body instanceof WithItem) {
/**
*处理 CTE(Common Table Expression,公用表表达式),即 WITH name AS (SELECT ...) SELECT ... 形式。
*/
WithItem wi = (WithItem) body;
processSelectBody(wi.getSelectBody(), removeBackticks);
}
}
/**
* 遍历并处理一个 SQL 语句中的各个部分(FROM、JOIN、SELECT、WHERE、GROUP BY、ORDER BY、HAVING),统一做别名/字段的引号或反引号处理。
*
* @param ps
* @param removeBackticks
*/
private void plainSelectHandle(PlainSelect ps, boolean removeBackticks) {
ColumnExpressionVisitor visitor = new ColumnExpressionVisitor(removeBackticks);
//如果 FROM 部分有表或子查询,就调用 handleFromItem 去处理它。
if (ps.getFromItem() != null) {
// handleFromItem(ps.getFromItem(), removeBackticks);
// ps.getFromItem().accept(new ColumnExpressionVisitor(removeBackticks));
handleFromItemWithVisitor(ps.getFromItem(), visitor);
}
;
//遍历所有 JOIN 子句
if (ps.getJoins() != null) {
ps.getJoins().forEach(j -> {
//处理 JOIN 的 FROM 部分
if (j.getRightItem() != null) {
// handleFromItem(j.getRightItem(), removeBackticks);
// j.getRightItem().accept(new ColumnExpressionVisitor(removeBackticks));
handleFromItemWithVisitor(j.getRightItem(), visitor);
}
//处理 JOIN 的 ON 部分
if (j.getOnExpression() != null) {
j.getOnExpression().accept(visitor);
}
//处理 JOIN 的 USING 部分
if (j.getUsingColumns() != null) {
// j.getUsingColumns().forEach(c -> quoteColumn(c, removeBackticks));
j.getUsingColumns().forEach(c -> c.accept(visitor));
}
});
}
//遍历 SELECT 里面的字段
if (ps.getSelectItems() != null) {
ps.getSelectItems().forEach(si -> {
//普通表达式 (SelectExpressionItem),
if (si instanceof SelectExpressionItem) {
SelectExpressionItem sei = (SelectExpressionItem) si;
//处理字段或函数里的列(比如 SUM(u.price))。
if (sei.getExpression() != null) {
sei.getExpression().accept(visitor);
}
//如果有别名(AS total),就调用 rewriteAlias 来处理别名是否加/去引号。
Alias alias = sei.getAlias();
if (alias != null) {
sei.setAlias(rewriteAlias(alias, removeBackticks));
}
} else if (si instanceof AllTableColumns) {
//AllTableColumns (table.*):如果是 t.* 这种形式,就调用 quoteTable 处理表名。
AllTableColumns atc = (AllTableColumns) si;
Table t = atc.getTable();
if (t != null) {
// quoteTable(t, removeBackticks);
t.accept(visitor);
}
}
});
}
//处理 WHERE,用 ColumnExpressionVisitor 处理。
if (ps.getWhere() != null) ps.getWhere().accept(visitor);
//遍历 GROUP BY 的字段或表达式,调用 ColumnExpressionVisitor。
if (ps.getGroupBy() != null && ps.getGroupBy().getGroupByExpressions() != null) {
ps.getGroupBy().getGroupByExpressions().forEach(expr -> expr.accept(visitor));
}
//遍历 ORDER BY 的字段/表达式,处理列名。
if (ps.getOrderByElements() != null) {
ps.getOrderByElements().forEach(o -> {
if (o.getExpression() != null) o.getExpression().accept(visitor);
});
}
//处理 HAVING
if (ps.getHaving() != null) ps.getHaving().accept(visitor);
//处理 GROUP BY 的别名
// fixGroupByAliases(ps);
}
// 新增:统一处理 FromItem(先处理 alias,再 accept 递归)
private void handleFromItemWithVisitor(FromItem fromItem, ColumnExpressionVisitor visitor) {
if (fromItem == null) return;
// 若 fromItem 是 Table,那么 Table.visit 会处理 alias(已有逻辑)
// 对于 SubSelect / ParenthesisFromItem 等,visitor 的 visit 方法中已做 alias 处理(上面添加)
fromItem.accept(visitor);
}
// 清空映射:在开始处理每条 SQL 前调用
private void clearAliasRenameMap() {
aliasRenameMap.get().clear();
}
// ---------- 生成安全别名(局部方法) ----------
private String generateSafeAlias(String original) {
String base = original == null ? "alias" : original.replaceAll("[^A-Za-z0-9_]", "");
if (base.isEmpty()) base = "alias";
// 保证以字母或下划线开头
if (!base.matches("^[A-Za-z_].*")) base = "_" + base;
String candidate = base + "_a";
int i = 0;
// 保证不与已有生成的别名冲突(检查 value)
while (aliasRenameMap.get().containsValue(candidate)) {
i++;
candidate = base + "_a" + i;
}
return candidate;
}
/**
* 配合 JSqlParser 的访问者模式,在遍历表达式树(表达式 AST)时,专门处理“列(Column)”相关的节点。
*/
private class ColumnExpressionVisitor extends ExpressionVisitorAdapter implements FromItemVisitor {
private final boolean removeBackticks;
ColumnExpressionVisitor(boolean removeBackticks) {
this.removeBackticks = removeBackticks;
}
/**
* 配合 JSqlParser 的访问者模式,在遍历表达式树(表达式 AST)时,专门处理“列(Column)”相关的节点。
*
* @param column
*/
@Override
public void visit(Column column) {
if (column == null) return;
String columnName = column.getColumnName();
if (columnName != null) {
if ((columnName.startsWith("`") && columnName.endsWith("`"))
|| (columnName.startsWith("[") && columnName.endsWith("]"))) {
columnName = columnName.substring(1, columnName.length() - 1);
}
if (removeBackticks) {
column.setColumnName(columnName);
}
}
//处理列名前面的表名
Table tbl = column.getTable();
if (tbl != null) {
String tblName = tbl.getName();
if (tblName != null) {
String norm = normalizeIdent(tblName);
if (removeBackticks) {
tbl.setName(norm);
}
String mapped = aliasRenameMap.get().get(norm.toUpperCase());
if (mapped != null) {
tbl.setName(mapped);
}
}
}
}
/**
* 当访问表达式树(AST)中遇到一个 Function 节点(例如 IFNULL(a,b)、DATE_FORMAT(d,fmt))时,会执行这个方法来处理该函数节点。
*
* @param function
*/
@Override
public void visit(Function function) {
if (function == null) return;
// Debug entry
logger.debug("visit(Function) called: {}", function.toString());
/* ------------------ 0) 先处理参数中可能嵌套的 GROUP_CONCAT(...)(优先级高) ------------------ */
try {
ExpressionList params = function.getParameters();
if (params != null && params.getExpressions() != null && !params.getExpressions().isEmpty()) {
List<Expression> exprs = params.getExpressions();
boolean anyReplaced = false;
// 遍历参数列表(拷贝索引用于安全修改)
for (int i = 0; i < exprs.size(); i++) {
Expression e = exprs.get(i);
if (e == null) continue;
// 递归先 visit 子表达式,保证列/别名等已被规范化
try {
e.accept(this);
} catch (Throwable ex) {
// 容错:子节点处理失败不影响整体流程
logger.debug("递归 accept 子表达式失败(忽略): {}", ex.getMessage());
}
// 大写文本用于简单检测(兼容不同 parser 表示差异)
String eTextUp = e.toString() == null ? "" : e.toString().toUpperCase(Locale.ROOT);
// 如果参数文本包含 GROUP_CONCAT,进入替换逻辑
if (eTextUp.contains("GROUP_CONCAT")) {
// 判断是否是复杂用法(DISTINCT / ORDER BY / SEPARATOR)
boolean complex = eTextUp.contains("DISTINCT") || eTextUp.contains("ORDER BY") || eTextUp.contains("SEPARATOR");
if (!complex) {
/* 简单场景:GROUP_CONCAT(arg) -> WM_CONCAT(arg)
这一步关键:要把 inner function 的参数正确复制给 WM_CONCAT,
若 AST 中没有参数(某些 parser 变体),用字符串解析回退。 */
Function wm = new Function();
wm.setName("WM_CONCAT");
if (e instanceof Function) {
// 常见且首选:内层确实是 Function 且带 parameters,直接 shallow-copy ExpressionList(不共享对象引用会更安全)
Function innerF = (Function) e;
ExpressionList innerParams = innerF.getParameters();
if (innerParams != null && innerParams.getExpressions() != null) {
List<Expression> copied = new ArrayList<>();
for (Expression pe : innerParams.getExpressions()) {
// 深拷贝策略:尝试 parse 表示形式得到独立 AST
try {
Expression parsed = CCJSqlParserUtil.parseExpression(pe.toString());
if (parsed != null) {
copied.add(parsed);
} else {
copied.add(pe);
}
} catch (Throwable parseEx) {
// 解析失败时退回原表达式节点(较安全)
copied.add(pe);
}
}
ExpressionList newPl = new ExpressionList();
newPl.setExpressions(copied);
wm.setParameters(newPl);
} else {
// innerF 没有参数(极少见),回退到文本解析
List<Expression> parsed = parseExpressionsFromCommaTopLevel(innerF.toString());
ExpressionList newPl = new ExpressionList();
newPl.setExpressions(parsed);
wm.setParameters(newPl);
}
} else {
// fallback:e 不是 Function(奇怪情形),尝试从文本中解析参数
List<Expression> parsed = parseExpressionsFromCommaTopLevel(e.toString());
ExpressionList newPl = new ExpressionList();
newPl.setExpressions(parsed);
wm.setParameters(newPl);
}
// 把参数位置替换为 WM_CONCAT(...) AST
exprs.set(i, wm);
anyReplaced = true;
logger.debug("Replaced nested GROUP_CONCAT -> WM_CONCAT: {}", wm);
} else {
/* 复杂场景:包含 DISTINCT/ORDER BY/SEPARATOR 等。我们尝试构造等价的 LISTAGG 写法并保留 NULL 语义:
CASE WHEN LISTAGG(...) IS NULL THEN NULL ELSE CONCAT(',', LISTAGG(...), ',') END
若构造/解析失败,保留原表达式(避免破坏语义)。 */
// 尝试识别聚合表达式和分隔符(尽可能保守)
Expression aggExpr = null;
String sep = ",";
if (e instanceof Function) {
Function innerF = (Function) e;
ExpressionList innerParams = innerF.getParameters();
if (innerParams != null && innerParams.getExpressions() != null && !innerParams.getExpressions().isEmpty()) {
for (Expression cand : innerParams.getExpressions()) {
if (cand == null) continue;
String cs = cand.toString().trim();
if (cs.toUpperCase(Locale.ROOT).startsWith("DISTINCT ")) {
String trimmed = cs.substring(8).trim();
try {
aggExpr = CCJSqlParserUtil.parseExpression(trimmed);
break;
} catch (Exception ex) {
aggExpr = cand;
break;
}
} else {
aggExpr = cand;
break;
}
}
}
// 如果文本里有 SEPARATOR 'x',尝试提取
if (eTextUp.contains("SEPARATOR")) {
Pattern p = Pattern.compile("SEPARATOR\\s+('([^']*)'|\\\"([^\\\"]*)\\\")",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
Matcher m = p.matcher(e.toString());
if (m.find()) {
String g = m.group(2) == null ? m.group(3) : m.group(2);
if (g != null) sep = g;
}
}
}
if (aggExpr == null) {
// 兜底从文本里尽量解析第一个参数
List<Expression> parsed = parseExpressionsFromCommaTopLevel(e.toString());
if (!parsed.isEmpty()) aggExpr = parsed.get(0);
}
if (aggExpr == null) aggExpr = new StringValue("''"); // 最终兜底
// 构建 LISTAGG-case SQL 文本(字符串),以保证 NULL 传播:
// CASE WHEN LISTAGG(agg, 'sep') WITHIN GROUP (ORDER BY agg) IS NULL THEN NULL ELSE CONCAT(',', LISTAGG(...), ',') END
String aggText = aggExpr.toString();
String listaggSql = "CASE WHEN LISTAGG(" + aggText + ", '" + sep + "') WITHIN GROUP (ORDER BY " + aggText + ") IS NULL THEN NULL ELSE CONCAT(',', LISTAGG(" + aggText + ", '" + sep + "') WITHIN GROUP (ORDER BY " + aggText + "), ',') END";
try {
Expression parsed = CCJSqlParserUtil.parseExpression(listaggSql);
exprs.set(i, parsed);
anyReplaced = true;
logger.debug("Replaced complex GROUP_CONCAT -> LISTAGG-case: {}", listaggSql);
} catch (Throwable ex) {
// 解析失败:记录并保留原始表达式(不做破坏性改写)
logger.warn("复杂 GROUP_CONCAT 转换为 LISTAGG 失败,保留原表达式: {} , error: {}", e, ex.getMessage());
}
}
}
} // end for parameters
// 写回参数(若有替换)
if (anyReplaced) {
function.setParameters(params);
}
}
} catch (Throwable ex) {
// 顶层容错:不让解析错误影响整体 SQL 执行
logger.debug("处理嵌套 GROUP_CONCAT 时发生异常(忽略): {}", ex.getMessage());
}
String name = function.getName();
//转换函数名,将其全部转化为大写
String nameUp = name == null ? "" : name.toUpperCase();
// 1) 把常见 MySQL 函数名映射为达梦等价
if ("IFNULL".equals(nameUp)) {
//IFNULL(x,y) → NVL(x,y)
function.setName("NVL");
} else if ("DATE_FORMAT".equals(nameUp)) {
//DATE_FORMAT(date, fmt) → TO_CHAR(date, fmt)(格式字符串另作转换)
function.setName("TO_CHAR");
} else if ("LEFT".equals(nameUp)) {
//LEFT(s,n) → SUBSTR(s,1,n)(会把参数结构改为三参数形式)
function.setName("SUBSTR");
} else if ("DATE_SUB".equals(nameUp) || "SUBDATE".equals(nameUp)) {
// 尝试把整个 DATE_SUB(...) 转为 ADD_MONTHS(...)
Expression converted = tryConvertToAddMonths(function);
if (converted instanceof Function) {
Function addMonths = (Function) converted;
function.setName(addMonths.getName());
function.setParameters(addMonths.getParameters());
// 递归处理参数(确保 dateExpr 被进一步处理,如 CURDATE())
if (function.getParameters() != null && function.getParameters().getExpressions() != null) {
for (Expression e : new ArrayList<>(function.getParameters().getExpressions())) {
if (e != null) e.accept(this);
}
}
return; // 已完成 DATE_SUB 处理
}
} else if ("CURDATE".equals(nameUp) || "NOW".equals(nameUp)) {
// CURDATE() / NOW() → SYSDATE(并移除参数,因为这些无参函数在 DM 中通常用 SYSDATE)
function.setName("SYSDATE");
function.setParameters(null);
}
/**
仅当函数带参数时进入。exprs 是参数表达式列表(可能包含 Column、Function、BinaryExpression、StringValue 等各种 Expression)。
// 2) 处理参数(对 DATE_FORMAT、LEFT、以及可能的 INTERVAL/DATE_SUB 情形)
*/
ExpressionList parameters = function.getParameters();
if (parameters != null && parameters.getExpressions() != null) {
//得到参数的列表
List<Expression> paramsList = parameters.getExpressions();
//把 SUBSTR(源自 LEFT)的 2 参数形式改成 3 参数形式
// LEFT -> SUBSTR: 参数由 (str, n) -> (str, 1, n)
if ("SUBSTR".equalsIgnoreCase(function.getName())) {
if (paramsList.size() == 2) {
//获取参数
Expression strExpr = paramsList.get(0);
Expression nExpr = paramsList.get(1);
//构造新参数
List<Expression> newExprs = new ArrayList<>();
newExprs.add(strExpr);
newExprs.add(new LongValue(1));
newExprs.add(nExpr);
function.getParameters().setExpressions(newExprs);
// 如果参数里面还有函数,则递归处理新参数
for (Expression e : new ArrayList<>(function.getParameters().getExpressions())) {
if (e != null) e.accept(this);
}
return;
}
}
//DATE_FORMAT -> TO_CHAR(复杂处理)
// TO_CHAR (原 DATE_FORMAT):处理格式字符串和可能嵌套的 INTERVAL 模式
//把 expr 中的 CURDATE() - INTERVAL N MONTH 之类写法(通常是 BinaryExpression)转换成 ADD_MONTHS(expr, -N)。
//把 MySQL 的格式字符串(例如 '%Y-%m')转换成 DM 的格式 'yyyy-mm'。
if ("TO_CHAR".equalsIgnoreCase(function.getName())) {
// 安全取出参数列表
Expression first = paramsList.size() > 0 ? paramsList.get(0) : null;
Expression second = paramsList.size() > 1 ? paramsList.get(1) : null;
// 尝试把 first 转为 ADD_MONTHS(...)(若匹配 INTERVAL / DATE_SUB / (expr - INTERVAL n MONTH))
Expression convertedFirst = tryConvertToAddMonths(first);
//如果 convertedFirst 是 Function 且函数名是 ADD_MONTHS,就把参数列表的第 0 个元素替换为这个 ADD_MONTHS 节点
if (convertedFirst != null && convertedFirst instanceof Function
&& "ADD_MONTHS".equalsIgnoreCase(((Function) convertedFirst).getName())) {
paramsList.set(0, convertedFirst);
} else if (convertedFirst != null && convertedFirst != first) {
//如果 helper 返回了不同的表达式(不是 ADD_MONTHS 但确实是被修改过的表达式,例如去掉了一层 Parenthesis、或作了其他变形),也把它写回去。
// helper 返回了不同的表达式(例如解开了括号并转换)
//确保 TO_CHAR 的第一个参数始终是已转换/规范化后的表达式,便于后续数据库兼容
paramsList.set(0, convertedFirst);
}
// 处理格式字符串:'%Y-%m' -> 'yyyy-mm'(只在格式是 StringValue 时处理)
if (second instanceof StringValue) {
StringValue sv = (StringValue) second;
sv.setValue(toDmDateFormat(sv.getValue()));
}
// 递归处理参数(包含可能新构造的 ADD_MONTHS 的参数)
for (Expression e : new ArrayList<>(paramsList)) {
if (e != null) e.accept(this);
}
// 写回参数(通常无需,但保持一致)
function.getParameters().setExpressions(paramsList);
return;
}
// 对其它函数参数做递归访问(能捕获嵌套的 CURDATE()/INTERVAL)
for (Expression e : new ArrayList<>(paramsList)) {
if (e != null) {
try {
e.accept(this);
} catch (Throwable ex) {
logger.debug("参数递归 accept 出错(忽略): {}", ex.getMessage());
}
}
}
}
}
//处理和关键字冲突的表名和别名
@Override
public void visit(Table table) {
if (table == null) return;
// 如果你已有去反引号/方括号的逻辑,可以保留;这里演示保留原有名称的清理
// 表名/模式名也做规范化(可选,但推荐)
if (table.getName() != null) {
table.setName(normalizeIdent(table.getName()));
}
if (table.getSchemaName() != null) {
table.setSchemaName(normalizeIdent(table.getSchemaName()));
}
// ---- 新增:若 table.name 实际上是之前记录在 aliasRenameMap 的别名(子查询别名/表别名),则把它替换成映射值 ----
Map<String, String> map = aliasRenameMap.get();
if (table.getName() != null) {
String normalized = normalizeIdent(table.getName());
String key = normalized == null ? null : normalized.toUpperCase(Locale.ROOT);
if (key != null) {
String mapped = map.get(key);
if (mapped != null) {
// 把 table 名替换为映射值(安全别名)
table.setName(mapped);
// 这个 table 本身没有 alias 对象(因为它本来就是一个别名引用,如 user.*),
// 所以我们不创建 alias,这样 toString 会输出 user_a.*(期望的形式)
}
}
}
Alias alias = table.getAlias();
if (alias != null) {
String raw = alias.getName();
String norm = normalizeIdent(raw);
alias.setName(norm); // 写回规范化别名(无包裹、无内空格)
table.setAlias(alias);
String key = norm.toUpperCase();
// 若别名是保留字,则生成安全别名并记录映射
Map<String, String> map2 = aliasRenameMap.get();
if (RESERVED_WORDS.contains(key)) {
if (map2.containsKey(key)) {
table.setAlias(new Alias(map2.get(key), false));
} else {
String newAlias = generateSafeAlias(norm);
table.setAlias(new Alias(newAlias, false));
// 注意:记录的是规范化后的大写
map2.putIfAbsent(key, newAlias);
}
} else {
// 即使不是保留字,也最好把别名清理一遍(去掉反引号/空格)
// 非保留字,使用规范化名(并且如果已有映射也不覆盖)
if (map2.containsKey(key)) {
table.setAlias(new Alias(map2.get(key), false));
} else {
table.setAlias(new Alias(norm, false));
}
}
}
// 如果需要,继续对 table 名本身(非别名)做其他处理
}
//————————————————————————————————————————————————处理表达式值为双引号的问题————————————————————————————————————————————————————————————
// 新增:专门处理比较表达式中的值部分
@Override
public void visit(Between expr) {
// 左右两端分别修复
if (expr.getBetweenExpressionStart() instanceof Column) {
Column c = (Column) expr.getBetweenExpressionStart();
String name = c.getColumnName();
if (isDoubleQuotedValue(name) && isProbablyLiteralName(name)) {
expr.setBetweenExpressionStart(new StringValue(unquoteAndToSqlLiteral(name)));
}
}
if (expr.getBetweenExpressionEnd() instanceof Column) {
Column c = (Column) expr.getBetweenExpressionEnd();
String name = c.getColumnName();
if (isDoubleQuotedValue(name) && isProbablyLiteralName(name)) {
expr.setBetweenExpressionEnd(new StringValue(unquoteAndToSqlLiteral(name)));
}
}
}
@Override
public void visit(EqualsTo expr) {
fixValueExpression(expr);
}
@Override
public void visit(NotEqualsTo expr) {
fixValueExpression(expr);
}
@Override
public void visit(LikeExpression expr) {
fixValueExpression(expr);
}
@Override
public void visit(GreaterThan expr) {
fixValueExpression(expr);
}
@Override
public void visit(GreaterThanEquals expr) {
fixValueExpression(expr);
}
@Override
public void visit(MinorThan expr) {
fixValueExpression(expr);
}
@Override
public void visit(MinorThanEquals expr) {
fixValueExpression(expr);
}
@Override
public void visit(InExpression expr) {
if (expr == null) return;
// 先访问左侧表达式(例如 user.dept_id),以便对表名/别名做替换
if (expr.getLeftExpression() != null) {
expr.getLeftExpression().accept(this);
}
// 再处理右侧的值列表
fixInExpressionValues(expr);
}
// 处理比较表达式中的值部分
private void fixValueExpression(BinaryExpression expr) {
// 先处理子节点
expr.getLeftExpression().accept(this);
expr.getRightExpression().accept(this);
// 用在 fixValueExpression 中替换右侧 / 左侧 double-quoted column
if (expr.getRightExpression() instanceof Column) {
Column col = (Column) expr.getRightExpression();
String name = col.getColumnName();
if (isDoubleQuotedValue(name) && isProbablyLiteralName(name)) {
expr.setRightExpression(new StringValue(unquoteAndToSqlLiteral(name))); // 注意:传入带单引号的字面量
}
}
if (expr.getLeftExpression() instanceof Column) {
Column col = (Column) expr.getLeftExpression();
String name = col.getColumnName();
if (isDoubleQuotedValue(name) && isProbablyLiteralName(name)) {
expr.setLeftExpression(new StringValue(unquoteAndToSqlLiteral(name)));
}
}
}
// 处理IN表达式中的值部分
private void fixInExpressionValues(InExpression expr) {
if (expr.getRightItemsList() instanceof ExpressionList) {
ExpressionList list = (ExpressionList) expr.getRightItemsList();
List<Expression> newExprs = new ArrayList<>();
for (Expression e : list.getExpressions()) {
if (e instanceof Column) {
Column col = (Column) e;
String name = col.getColumnName();
if (isDoubleQuotedValue(name) && isProbablyLiteralName(name)) {
newExprs.add(new StringValue(unquoteAndToSqlLiteral(name)));
} else {
newExprs.add(e);
}
} else {
newExprs.add(e);
}
}
list.setExpressions(newExprs);
}
}
private boolean isDoubleQuotedValue(String value) {
return value != null && value.length() > 1 && value.startsWith("\"") && value.endsWith("\"");
}
/**
* 判断一个双引号包裹的名称是否更像“字面量值”而非合法标识符。
* 若返回 true,表示我们应把它转换为 StringValue(即把 "xxx" -> 'xxx')
* <p>
* 启发式规则(可根据项目特点调整):
* - 包含空格、中文、特殊标点(如:-、:、/、\)或非 ASCII 字符 -> 很可能是字面量
* - 或以数字开头或包含小括号、逗号等 -> 很可能不是普通标识符
* - 否则若匹配合法标识符正则 ^[A-Za-z_][A-Za-z0-9_\\$]*$ 则更像标识符 -> 返回 false
*/
private boolean isProbablyLiteralName(String quoted) {
if (!isDoubleQuotedValue(quoted)) return false;
String inner = quoted.substring(1, quoted.length() - 1);
// 若内含中文或空格等,认为是值:
if (inner.matches(".*[\\p{IsHan}\\s].*")) return true; // 含中文或空格
// 若含有明显的非标识符字符:
if (inner.matches(".*[^A-Za-z0-9_\\$].*")) return true;
// 若以数字开头也更可能是值
if (inner.matches("^[0-9].*")) return true;
// 若能匹配常规标识符规则,则视作标识符(不要替换)
if (inner.matches("^[A-Za-z_][A-Za-z0-9_\\$]*$")) return false;
// 兜底当作字面量
return true;
}
// 注意:返回值是带单引号包裹并且已转义的 SQL 字面量字符串,适合直接用在 new StringValue(...)
private String unquoteAndToSqlLiteral(String value) {
// value 形如: "张三" 或 """" 内含转义等
if (value == null || value.length() < 2) return "''";
String inner = value.substring(1, value.length() - 1); // 去掉首尾双引号
// 1) 处理双引号内部的 SQL 转义:SQL 中双引号内部要表示一个双引号通常是两个双引号("" -> ")
// 把成单个双引号(这是对原始双引号字面量的反转义)
inner = inner.replace("\"\"", "\"");
// 2) 为了把它变为单引号字符串字面量,要对单引号进行转义(' -> '')
inner = inner.replace("'", "''");
// 3) 最终以单引号包裹返回(StringValue 的构造通常期望带引号的字面量)
return "'" + inner + "'";
}
//————————————————————————————————————————————————处理表达式值为双引号的问题————————————————————————————————————————————————————————————
//处理select的子查询
@Override
public void visit(SubSelect subSelect) {
if (subSelect == null) return;
// --- 处理子查询的 alias(若存在): 规范化、去引号、保留字替换并写入映射 ---
Alias alias = subSelect.getAlias();
if (alias != null) {
// 先剥离包裹符并写回(与 rewriteAlias 行为一致)
Alias rewritten = rewriteAlias(alias, removeBackticks);
subSelect.setAlias(rewritten);
// 规范化名称并检查保留字
String norm = normalizeIdent(rewritten.getName());
if (norm != null) {
String key = norm.toUpperCase(Locale.ROOT);
Map<String, String> map = aliasRenameMap.get();
if (RESERVED_WORDS.contains(key)) {
if (map.containsKey(key)) {
// 已有生成的安全别名,复用它
subSelect.setAlias(new Alias(map.get(key), false));
} else {
String newAlias = generateSafeAlias(norm);
subSelect.setAlias(new Alias(newAlias, false));
map.putIfAbsent(key, newAlias);
}
} else {
// 非保留字,使用规范化名(并且如果已有映射也不覆盖)
if (map.containsKey(key)) {
subSelect.setAlias(new Alias(map.get(key), false));
} else {
subSelect.setAlias(new Alias(norm, false));
}
}
}
}
// 递归处理子查询体
if (subSelect.getSelectBody() != null) {
processSelectBody(subSelect.getSelectBody(), removeBackticks);
}
}
@Override
public void visit(SubJoin subJoin) {
if (subJoin == null) return;
if (subJoin.getLeft() != null) {
subJoin.getLeft().accept(this); // 递归处理左侧 from item
}
if (subJoin.getJoinList() != null) {
for (Join j : subJoin.getJoinList()) {
if (j.getRightItem() != null) {
j.getRightItem().accept(this);
}
}
}
}
@Override
public void visit(LateralSubSelect lateralSubSelect) {
if (lateralSubSelect == null) return;
SubSelect ss = lateralSubSelect.getSubSelect();
if (ss != null) {
// 如果 lateral 本身有 alias(部分实现会把 alias 放在 SubSelect),也会在 SubSelect 的 visit 中被处理
ss.accept((FromItemVisitor) this);
}
}
@Override
public void visit(ValuesList valuesList) {
// 如果你不需要特殊处理 VALUES 列表,可空实现
}
@Override
public void visit(TableFunction tableFunction) {
if (tableFunction == null) return;
// 若 TableFunction 内有 Function,可按需处理其函数参数
if (tableFunction.getFunction() != null) {
tableFunction.getFunction().accept(this);
}
}
@Override
public void visit(ParenthesisFromItem parenthesisFromItem) {
if (parenthesisFromItem == null) return;
// 规范化并处理 alias(若存在)
Alias alias = parenthesisFromItem.getAlias();
if (alias != null) {
Alias rewritten = rewriteAlias(alias, removeBackticks);
parenthesisFromItem.setAlias(rewritten);
String norm = normalizeIdent(rewritten.getName());
if (norm != null && RESERVED_WORDS.contains(norm.toUpperCase(Locale.ROOT))) {
String newAlias = generateSafeAlias(norm);
parenthesisFromItem.setAlias(new Alias(newAlias, false));
aliasRenameMap.get().putIfAbsent(norm.toUpperCase(Locale.ROOT), newAlias);
} else if (norm != null) {
parenthesisFromItem.setAlias(new Alias(norm, false));
}
}
if (parenthesisFromItem.getFromItem() != null) {
parenthesisFromItem.getFromItem().accept(this);
}
}
}
/**
* 从 INTERVAL ... MONTH 这类表达式中提取数字 N(多少月)。
*
* @param intervalExpr
* @return
*/
private int extractIntervalMonths(Expression intervalExpr) {
if (intervalExpr == null) return 0;
try {
// 优先针对常见节点类型
// 数字节点
if (intervalExpr instanceof LongValue) {
return (int) ((LongValue) intervalExpr).getValue();
}
// 字符串节点
if (intervalExpr instanceof StringValue) {
String s = ((StringValue) intervalExpr).getValue();
return Integer.parseInt(s.trim());
}
// 回退:从文本中抽取第一个数字(兼容性保底)
String s = intervalExpr.toString();
Matcher m = Pattern.compile("(\\d+)").matcher(s);
if (m.find()) {
return Integer.parseInt(m.group(1));
}
} catch (Throwable ignored) {
}
return 0;
}
//解决转换成DM时,转换成add_months()
/**
* 把 MySQL 的 DATE_SUB(..., INTERVAL n MONTH) 或者 date - INTERVAL n MONTH 表达式,转换成达梦数据库的 ADD_MONTHS(date, -n)。
* 在 AST 遍历过程中,当遇到 DATE_SUB 或者类似 date - INTERVAL 表达式时,调用此方法尝试转换。
*
* @param expr
* @return
*/
private Expression tryConvertToAddMonths(Expression expr) {
if (expr == null) return null;
// 1) Parenthesis: 解开括号再处理
/**
* 如果表达式是括号包裹的(例如 (DATE_SUB(...))),则直接取里面的表达式 p.getExpression() 再递归调用本方法。
* 忽略无意义的括号,确保能处理到真实的表达式。
*/
if (expr instanceof Parenthesis) {
Parenthesis p = (Parenthesis) expr;
return tryConvertToAddMonths(p.getExpression());
}
// 2) BinaryExpression: left - INTERVAL N MONTH
/**
* 检查是否是二元表达式(BinaryExpression),即形如 date - INTERVAL 2 MONTH 这种 SQL 结构。
* 取出左右子表达式:left 通常是日期字段/函数,right 通常是 INTERVAL n MONTH。
*/
if (expr instanceof BinaryExpression) {
BinaryExpression be = (BinaryExpression) expr;
//left 通常是日期字段/函数
Expression left = be.getLeftExpression();
//right 通常是 INTERVAL n MONTH
Expression right = be.getRightExpression();
//确保右边表达式不为空。
if (right != null) {
//将右边表达式转成字符串并大写,方便做关键字匹配(比如 "INTERVAL 2 MONTH")
String rightText = right.toString().toUpperCase();
//判断右边是不是包含 INTERVAL 和 MONTH,即是不是 INTERVAL n MONTH 结构
if (rightText.contains("INTERVAL") && rightText.contains("MONTH")) {
//调用辅助方法 extractIntervalMonths(right),从右侧表达式中解析出月份数(整数值 n)。
//例如 INTERVAL 2 MONTH → 2。
int months = extractIntervalMonths(right);
//新建一个 Function 对象,表示目标数据库的 ADD_MONTHS(...) 函数
Function addMonths = new Function();
addMonths.setName("ADD_MONTHS");
//创建一个参数列表容器 ExpressionList,里面维护一个 List<Expression>,用于存放函数参数。
ExpressionList el = new ExpressionList();
List<Expression> params = new ArrayList<>();
//把 left(即原来的日期部分)作为第一个参数。
params.add(left);
//把负的月份数作为第二个参数(DATE_SUB 的语义是“往前减去 n 个月”,而 ADD_MONTHS 正常是“往后加 n 个月”,所以要用负号来抵消差异)
params.add(new LongValue(-months)); // 关键:负整数
//把参数放入 ExpressionList,再设置到函数对象上
el.setExpressions(params);
addMonths.setParameters(el);
return addMonths;
}
}
}
// 3) Function: DATE_SUB(date, INTERVAL n MONTH) 或 SUBDATE(...)
//如果表达式本身是一个函数调用(Function),就取函数名。
if (expr instanceof Function) {
Function f = (Function) expr;
//转大写
String fname = f.getName() == null ? "" : f.getName().toUpperCase();
//判断函数名是否是 DATE_SUB 或 SUBDATE
if ("DATE_SUB".equals(fname) || "SUBDATE".equals(fname)) {
//确认函数有参数列表,并且参数数目至少有两个(第一个是日期,第二个是 INTERVAL n MONTH)
if (f.getParameters() != null && f.getParameters().getExpressions() != null) {
//取出参数列表
List<Expression> p = f.getParameters().getExpressions();
if (p.size() >= 2) {
//取第一个参数(日期部分)
Expression dateExpr = p.get(0);
//第二个参数(间隔部分)
Expression intervalExpr = p.get(1);
//调用辅助方法解析间隔里的月份数
int months = extractIntervalMonths(intervalExpr);
//新建一个 Function 函数对象,表示目标数据库的 ADD_MONTHS(...) 函数
Function addMonths = new Function();
addMonths.setName("ADD_MONTHS");
//创建一个参数列表容器 ExpressionList,里面维护一个 List<Expression>,用于存放函数参数。
ExpressionList el = new ExpressionList();
List<Expression> params = new ArrayList<>();
//把日期部分作为第一个参数
params.add(dateExpr);
//负的月份数作为第二个参数
params.add(new LongValue(-months));
el.setExpressions(params);
addMonths.setParameters(el);
return addMonths;
}
}
}
}
// 4) 无法识别,返回原表达式(上层继续递归)
return expr;
}
private String toDmDateFormat(String mysqlFmt) {
if (mysqlFmt == null) return null;
String f = mysqlFmt.trim();
if ((f.startsWith("'") && f.endsWith("'")) || (f.startsWith("\"") && f.endsWith("\""))) {
f = f.substring(1, f.length() - 1);
}
// 常见替换:%Y -> yyyy, %m -> mm, %d -> dd
f = f.replaceAll("%Y", "yyyy")
.replaceAll("%y", "yy")
.replaceAll("%m", "mm")
.replaceAll("%d", "dd");
return f;
}
private Alias rewriteAlias(Alias alias, boolean removeBackticks) {
if (alias == null) return null;
String aName = alias.getName();
if (aName == null) return alias;
// 去掉左右两端的空白
aName = aName.trim();
// 循环剥离最外层的包裹符,直到不再匹配。
// 支持的包裹符对包括:`...`, "..." , '...' , [...]
while (aName.length() >= 2) {
char first = aName.charAt(0);
char last = aName.charAt(aName.length() - 1);
boolean matched = false;
if (first == '`' && last == '`') {
aName = aName.substring(1, aName.length() - 1);
matched = true;
} else if (first == '"' && last == '"') {
aName = aName.substring(1, aName.length() - 1);
matched = true;
} else if (first == '\'' && last == '\'') {
aName = aName.substring(1, aName.length() - 1);
matched = true;
} else if (first == '[' && last == ']') {
aName = aName.substring(1, aName.length() - 1);
matched = true;
}
if (!matched) break;
aName = aName.trim(); // 去掉剥离后可能残留的空白
}
if (removeBackticks) {
// 将剥离后的 alias 写回 Alias 对象
alias.setName(aName);
}
return alias;
}
//统一的“标识符规范化”工具
private static String normalizeIdent(String s) {
if (s == null) return null;
String x = s.trim();
while (x.length() >= 2) {
char f = x.charAt(0), l = x.charAt(x.length() - 1);
boolean wrapped =
(f == '`' && l == '`') ||
(f == '"' && l == '"') ||
(f == '[' && l == ']') ||
(f == '\'' && l == '\'');
if (!wrapped) break;
x = x.substring(1, x.length() - 1).trim();
}
return x;
}
/* ------------------ 辅助方法:从文本中解析函数括号内部的顶层逗号分割表达式列表 ------------------ */
/**
* 输入类似 "GROUP_CONCAT(col1, col2)" 或 "GROUP_CONCAT( some_complex(expr), '|' )",
* 返回解析后的 Expression 列表(尽量解析为 AST;解析失败时以 StringValue 退回)。
* <p>
* 设计要点:
* - 使用正则非贪婪抓取括号内部文本(DOTALL 支持跨行)
* - 然后按“顶层逗号”分割(跳过嵌套括号/字符串内部的逗号)
* - 最后对每个片段尝试 CCJSqlParserUtil.parseExpression(...)
*/
private List<Expression> parseExpressionsFromCommaTopLevel(String funcText) {
List<Expression> out = new ArrayList<>();
if (funcText == null) return out;
// 1) 提取第一个匹配的括号内容:GROUP_CONCAT\s*\((.*?)\)
Pattern p = Pattern.compile("GROUP_CONCAT\\s*\\((.*?)\\)",
Pattern.CASE_INSENSITIVE | Pattern.DOTALL);
Matcher m = p.matcher(funcText);
String inside;
if (m.find()) {
inside = m.group(1);
} else {
// 若不是 GROUP_CONCAT 字样(回退取整个 funcText 的括号内)
Pattern p2 = Pattern.compile("\\((.*?)\\)",
Pattern.DOTALL);
Matcher m2 = p2.matcher(funcText);
inside = m2.find() ? m2.group(1) : null;
}
if (inside == null) return out;
// 2) 按顶层逗号分割成片段(跳过括号/单双引号内的逗号)
List<String> parts = splitTopLevelComma(inside);
// 3) 对每个片段尝试解析为 Expression(优先),失败时退回 StringValue(带原样)
for (String part : parts) {
String tok = part == null ? "" : part.trim();
if (tok.isEmpty()) continue;
try {
Expression parsed = CCJSqlParserUtil.parseExpression(tok);
out.add(parsed);
} catch (Throwable ex) {
// 解析失败则把原文本作为 StringValue(注意需要以 SQL 字面量形式决定是否带引号)
out.add(new StringValue(tok));
}
}
return out;
}
/**
* 按“顶层逗号”把字符串分割成片段,跳过在括号或引号内部的逗号。
* 这是一个小型状态机:跟踪 parentheses depth、单双引号状态。
*/
private List<String> splitTopLevelComma(String s) {
List<String> parts = new ArrayList<>();
if (s == null) return parts;
StringBuilder cur = new StringBuilder();
int depth = 0;
boolean inSingle = false;
boolean inDouble = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
// 处理引号状态(注意 SQL 双引号/单引号可能用法不同,但这里做一个通用处理)
if (c == '\'' && !inDouble) {
inSingle = !inSingle;
cur.append(c);
continue;
} else if (c == '\"' && !inSingle) {
inDouble = !inDouble;
cur.append(c);
continue;
}
if (!inSingle && !inDouble) {
if (c == '(') {
depth++;
} else if (c == ')') {
if (depth > 0) depth--;
} else if (c == ',' && depth == 0) {
// 顶层逗号:切分
parts.add(cur.toString());
cur.setLength(0);
continue;
}
}
cur.append(c);
}
// 剩余部分
if (cur.length() > 0) parts.add(cur.toString());
return parts;
}
/* --------------------- 工具方法 --------------------- */
/**
* 清空缓存(测试或运维用)
*/
public void clearCache() {
sqlCache.clear();
}
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zdsc.dm.autoconfig.DmSqlRewriteAutoConfiguration
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)