├── .intellijPlatform ├── self-update.lock └── localPlatformArtifacts │ ├── IU-243.25659.39 │ └── bundledPlugin-com.intellij.java-IU-243.25659.39.xml │ └── IU-243.23654.117 │ └── bundledPlugin-com.intellij.java-IU-243.23654.117.xml ├── libs └── ojdbc6-11.2.0.4.jar ├── src └── main │ ├── resources │ ├── soar.exe │ ├── soar.yaml │ └── META-INF │ │ ├── pluginIcon.svg │ │ └── plugin.xml │ └── java │ └── com │ └── chen │ ├── constant │ ├── DbConstant.java │ ├── SqlConstants.java │ ├── FileConstant.java │ ├── DataSourceConstants.java │ └── MessageConstants.java │ ├── action │ ├── ShowERDiagramAction.java │ ├── DbConfigManageAction.java │ ├── ViewRealSqlAction.java │ ├── DbConfigAddAction.java │ ├── PrettySqlAction.java │ ├── CheckSqlAction.java │ ├── SqlTableDocumentationProvider.java │ ├── ExplainSqlAction.java │ └── ERDiagramToolWindowFactory.java │ ├── utils │ ├── StringUtils.java │ ├── DataSourceUrlFixer.java │ ├── SqlCheckUtil.java │ ├── ColumnMetaUtils.java │ ├── SqlFormatUtil.java │ ├── DataSourceManager.java │ ├── HtmlViewerUtil.java │ ├── SqlParamUtils.java │ ├── SoarYamlUtil.java │ ├── JdbcTableInfoUtil.java │ └── DbConfigUtil.java │ ├── entity │ ├── TableMeta.java │ ├── DbConfig.java │ ├── ColumnMeta.java │ └── SoarConfig.java │ └── dialog │ ├── ParamInputDialog.java │ └── SqlPreviewDialog.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle ├── gradle.properties ├── .gitignore ├── .run └── Run IDE with Plugin.run.xml ├── README.md ├── gradlew.bat ├── README_EN.md └── gradlew /.intellijPlatform/self-update.lock: -------------------------------------------------------------------------------- 1 | 2025-06-13 -------------------------------------------------------------------------------- /libs/ojdbc6-11.2.0.4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiYuan-2002/PrettySQL/HEAD/libs/ojdbc6-11.2.0.4.jar -------------------------------------------------------------------------------- /src/main/resources/soar.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiYuan-2002/PrettySQL/HEAD/src/main/resources/soar.exe -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SiYuan-2002/PrettySQL/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenCentral() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | rootProject.name = "PrettySQL" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Opt-out flag for bundling Kotlin standard library -> https://jb.gg/intellij-platform-kotlin-stdlib 2 | kotlin.stdlib.default.dependency=false 3 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 4 | org.gradle.configuration-cache=true 5 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 6 | org.gradle.caching=true 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle/ 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !gradle/wrapper/gradle-wrapper.properties 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iml 10 | *.iws 11 | *.ipr 12 | out/ 13 | 14 | ### Eclipse ### 15 | .classpath 16 | .project 17 | .settings/ 18 | bin/ 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ 26 | 27 | ### VS Code ### 28 | .vscode/ 29 | 30 | ### macOS ### 31 | .DS_Store 32 | 33 | ### Windows ### 34 | Thumbs.db 35 | ehthumbs 36 | -------------------------------------------------------------------------------- /src/main/java/com/chen/constant/DbConstant.java: -------------------------------------------------------------------------------- 1 | package com.chen.constant; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: 默认数据库连接兼容常量 7 | * @date 2025/6/13 7:12 8 | */ 9 | public class DbConstant { 10 | 11 | public static final String MYSQL_URL_FIX = "?useSSL=false&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useInformationSchema=false"; 12 | public static final String SQLSERVER_URL_FIX = ";encrypt=true;trustServerCertificate=true"; 13 | public static final String ORACLE_URL_FIX = ""; 14 | public static final String DEFAULT_MYSQL = "jdbc:mysql://127.0.0.1:3306/db"; 15 | 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/chen/constant/SqlConstants.java: -------------------------------------------------------------------------------- 1 | package com.chen.constant; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: SQL相关常量维护 7 | * @date 2025/6/14 8:42 8 | */ 9 | public class SqlConstants { 10 | 11 | // SQL关键词(统一小写) 12 | public static final String SQL_DELETE = "delete"; 13 | public static final String SQL_UPDATE = "update"; 14 | public static final String SQL_WHERE = "where"; 15 | 16 | // 警告和错误提示(可根据需要迁移到国际化资源文件) 17 | public static final String WARN_UNSAFE_DELETE_UPDATE = 18 | "SQL检查通过,警告:检测到未加 WHERE 的 DELETE/UPDATE 语句,可能会影响全表数据!"; 19 | 20 | public static final String ERROR_SQL_EXECUTE_PREFIX = "SQL执行错误: "; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/ShowERDiagramAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.intellij.openapi.actionSystem.AnAction; 4 | import com.intellij.openapi.actionSystem.AnActionEvent; 5 | import com.intellij.openapi.wm.ToolWindow; 6 | import com.intellij.openapi.wm.ToolWindowManager; 7 | 8 | /** 9 | * @author czh 10 | * @version 1.0 11 | * @description: 12 | * @date 2025/6/24 7:11 13 | */ 14 | public class ShowERDiagramAction extends AnAction { 15 | 16 | @Override 17 | public void actionPerformed(AnActionEvent e) { 18 | ToolWindow toolWindow = ToolWindowManager.getInstance(e.getProject()).getToolWindow("ER Diagram"); 19 | if (toolWindow != null) { 20 | toolWindow.show(); 21 | } 22 | } 23 | } 24 | 25 | 26 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/com/chen/constant/FileConstant.java: -------------------------------------------------------------------------------- 1 | package com.chen.constant; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: 文件常量 7 | * @date 2025/6/13 7:12 8 | */ 9 | public class FileConstant { 10 | 11 | public static final String CONFIG_PATH = ".idea/db-config.json"; 12 | public static final String CONFIG_PATH_ALL = ".idea/db-all.json"; 13 | public static final String XML = "xml"; 14 | public static final String SOARYMAL_PATH = ".idea/soar.yaml"; 15 | public static final String SQL_SCRIPT_FILE_NAME = ".idea/executeSqlFile.sql"; 16 | public static final String IDEA_DIR = ".idea"; 17 | public static final String SOAR_EXE_NAME = "soar.exe"; 18 | public static final String SQL_FILE_NAME = "executeSqlFile.sql"; 19 | public static final String YAML_FILE_NAME = "soar.yaml"; 20 | public static final String RESULT_DIR = "C:\\result"; 21 | public static final String REPORT_PREFIX = "SQL分析报告_"; 22 | public static final String HTML_SUFFIX = ".html"; 23 | public static final String MD_SUFFIX = ".md"; 24 | public static final String DATE_FORMAT = "yyyy-MM-dd_HH-mm-ss"; 25 | public static final String D3_JS_PATH = "static/js/d3.v7.min.js"; 26 | public static final String DAGRE_JS_PATH = "static/js/dagre-d3.min.js"; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/resources/soar.yaml: -------------------------------------------------------------------------------- 1 | # 线上环境配置 2 | online-dsn: 3 | addr: 127.0.0.1:3306 4 | schema: ry 5 | user: root 6 | password: root 7 | disable: false 8 | # 测试环境配置 9 | test-dsn: 10 | addr: 127.0.0.1:3306 11 | schema: ry 12 | user: root 13 | password: root 14 | disable: false 15 | # 是否允许测试环境与线上环境配置相同 16 | allow-online-as-test: true 17 | # 是否清理测试时产生的临时文件 18 | drop-test-temporary: true 19 | # 语法检查小工具 20 | only-syntax-check: false 21 | sampling-statistic-target: 100 22 | sampling: false 23 | # 日志级别,[0:Emergency, 1:Alert, 2:Critical, 3:Error, 4:Warning, 5:Notice, 6:Informational, 7:Debug] 24 | log-level: 7 25 | log-output: '' 26 | # 优化建议输出格式 27 | report-type: html 28 | ignore-rules: 29 | - "" 30 | # 黑名单中的 SQL 将不会给评审意见。一行一条 SQL,可以很正则也可以是指纹,填写指纹时注意问号需要加反斜线转义。 31 | blacklist: ${your_config_dir}/soar.blacklist 32 | # 启发式算法相关配置 33 | max-join-table-count: 5 34 | max-group-by-cols-count: 5 35 | max-distinct-count: 5 36 | max-index-cols-count: 5 37 | max-total-rows: 9999999 38 | spaghetti-query-length: 2048 39 | allow-drop-index: false 40 | # EXPLAIN相关配置 41 | explain-sql-report-type: pretty 42 | explain-type: extended 43 | explain-format: traditional 44 | explain-warn-select-type: 45 | - "" 46 | explain-warn-access-type: 47 | - ALL 48 | explain-max-keys: 3 49 | explain-min-keys: 0 50 | explain-max-rows: 10000 51 | explain-warn-extra: 52 | - "" 53 | explain-max-filtered: 100 54 | explain-warn-scalability: 55 | - O(n) 56 | query: "" 57 | list-heuristic-rules: false 58 | list-test-sqls: false 59 | verbose: true -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/StringUtils.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: 7 | * @date 2025/6/12 15:59 8 | */ 9 | public class StringUtils { 10 | /** 11 | * 判断字符串不为 null、不为 "null"(忽略大小写)、不为 "" 或仅空白 12 | * 13 | * @param str 待判断字符串 14 | * @return 合法返回 true,否则 false 15 | */ 16 | public static boolean notBlankAndNotNullStr(String str) { 17 | return str != null && !"null".equalsIgnoreCase(str.trim()) && !str.trim().isEmpty(); 18 | } 19 | 20 | /** 21 | * 判断字符串为 null、"null"(忽略大小写)、"" 或仅空白 22 | * 23 | * @param str 待判断字符串 24 | * @return 无效返回 true,否则 false 25 | */ 26 | public static boolean isBlankOrNullStr(String str) { 27 | return str == null || "null".equalsIgnoreCase(str.trim()) || str.trim().isEmpty(); 28 | } 29 | 30 | /** 31 | * 去除字符串首尾空白并安全返回,若为 null 则返回空串 32 | * 33 | * @param str 待处理字符串 34 | * @return 去空白后字符串或 "" 35 | */ 36 | public static String trimToEmpty(String str) { 37 | return str == null ? "" : str.trim(); 38 | } 39 | 40 | /** 41 | * 判断两个字符串相等,忽略大小写和首尾空白,支持 null 42 | * 43 | * @param str1 字符串1 44 | * @param str2 字符串2 45 | * @return 相等返回 true,否则 false 46 | */ 47 | public static boolean equalsIgnoreCaseAndBlank(String str1, String str2) { 48 | if (str1 == null && str2 == null) return true; 49 | if (str1 == null || str2 == null) return false; 50 | return str1.trim().equalsIgnoreCase(str2.trim()); 51 | } 52 | 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/DataSourceUrlFixer.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.constant.DataSourceConstants; 4 | import com.chen.constant.DbConstant; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * 数据库 URL 补全工具类。 10 | *

11 | * 不同数据库类型连接 URL 可能需要追加特定的参数(如字符集、时区等)。 12 | * 本类通过数据库类型自动匹配并返回对应的 URL 补全片段。 13 | * 例如: 14 | * - MySQL: ?useSSL=false&characterEncoding=utf8 15 | * - SQLServer: ;encrypt=true 16 | * - Oracle: (如不需要补全,可为空字符串) 17 | *

18 | * 用法示例: 19 | *
20 |  *     String dbType = parseDbType(url);
21 |  *     String fullUrl = url + DataSourceUrlFixer.appendUrlFix(dbType, url);
22 |  * 
23 | * 24 | * @author czh 25 | * @version 1.0 26 | * @date 2025/6/17 15:00 27 | */ 28 | public class DataSourceUrlFixer { 29 | 30 | /** 31 | * 数据库类型与对应的 URL 补全片段映射。 32 | * 用于连接 JDBC 参数时自动拼接标准参数。 33 | */ 34 | private static final Map DB_TYPE_TO_URL_FIX = Map.of( 35 | DataSourceConstants.DB_TYPE_MYSQL, DbConstant.MYSQL_URL_FIX, 36 | DataSourceConstants.DB_TYPE_SQLSERVER, DbConstant.SQLSERVER_URL_FIX, 37 | DataSourceConstants.DB_TYPE_ORACLE, DbConstant.ORACLE_URL_FIX 38 | ); 39 | 40 | /** 41 | * 根据数据库类型返回对应的 URL 补全字符串。 42 | *

43 | * 若该数据库类型无补全片段,返回空字符串。 44 | *

45 | * 46 | * @param dbType 数据库类型(如 "mysql"、"oracle") 47 | * @param url 原始数据库连接 URL(未使用,仅预留) 48 | * @return URL 补全片段(以 `?` 或 `;` 开头) 49 | */ 50 | public static String appendUrlFix(String dbType, String url) { 51 | return DB_TYPE_TO_URL_FIX.getOrDefault(dbType, ""); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/chen/entity/TableMeta.java: -------------------------------------------------------------------------------- 1 | package com.chen.entity; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * 表结构元数据类 7 | * 包含表名、表注释、字段列表信息 8 | * 可用于数据库结构展示、SQL 解析等场景 9 | * 10 | * @author czh 11 | * @version 2.1 12 | * @date 2025/06/20 13 | */ 14 | public class TableMeta { 15 | 16 | /** 表名(英文名称) */ 17 | private String tableName; 18 | 19 | /** 表备注(中文名称/说明) */ 20 | private String tableComment; 21 | 22 | /** 表字段列表 */ 23 | private List columns; 24 | 25 | public TableMeta() {} 26 | 27 | public TableMeta(String tableName, String tableComment, List columns) { 28 | this.tableName = tableName; 29 | this.tableComment = tableComment; 30 | this.columns = columns; 31 | } 32 | 33 | public String getTableName() { 34 | return tableName; 35 | } 36 | 37 | public TableMeta setTableName(String tableName) { 38 | this.tableName = tableName; 39 | return this; 40 | } 41 | 42 | public String getTableComment() { 43 | return tableComment; 44 | } 45 | 46 | public TableMeta setTableComment(String tableComment) { 47 | this.tableComment = tableComment; 48 | return this; 49 | } 50 | 51 | public List getColumns() { 52 | return columns; 53 | } 54 | 55 | public TableMeta setColumns(List columns) { 56 | this.columns = columns; 57 | return this; 58 | } 59 | 60 | @Override 61 | public String toString() { 62 | return "TableMeta{" + 63 | "tableName='" + tableName + '\'' + 64 | ", tableComment='" + tableComment + '\'' + 65 | ", columns=" + columns + 66 | '}'; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/chen/constant/DataSourceConstants.java: -------------------------------------------------------------------------------- 1 | package com.chen.constant; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: 数据源常量类 7 | * @date 2025/6/14 10:17 8 | */ 9 | public class DataSourceConstants { 10 | 11 | // ================== 驱动类名 ================== 12 | public static final String MYSQL_DRIVER = "com.mysql.cj.jdbc.Driver"; 13 | public static final String ORACLE_DRIVER = "oracle.jdbc.OracleDriver"; 14 | public static final String SQLSERVER_DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; 15 | public static final String POSTGRESQL_DRIVER = "org.postgresql.Driver"; 16 | public static final String SQLITE_DRIVER = "org.sqlite.JDBC"; 17 | public static final String H2_DRIVER = "org.h2.Driver"; 18 | public static final String DM_DRIVER = "dm.jdbc.driver.DmDriver"; // 达梦 19 | public static final String KINGBASE_DRIVER = "com.kingbase8.Driver"; // 人大金仓 20 | public static final String DB2_DRIVER = "com.ibm.db2.jcc.DB2Driver"; 21 | 22 | // ================== 连接池配置 ================== 23 | public static final int MAX_POOL_SIZE = 5; // 最大连接数 24 | public static final int MIN_IDLE = 1; // 最小空闲连接数 25 | public static final long CONNECTION_TIMEOUT = 5000L; // 获取连接超时时间(ms) 26 | public static final long IDLE_TIMEOUT = 30000L; // 空闲连接超时时间(ms) 27 | public static final long MAX_LIFETIME = 60000L; // 最大连接生存时间(ms) 28 | 29 | // ================== 支持的数据库类型 ================== 30 | public static final String DB_TYPE_MYSQL = "mysql"; 31 | public static final String DB_TYPE_ORACLE = "oracle"; 32 | public static final String DB_TYPE_SQLSERVER = "sqlserver"; 33 | public static final String DB_TYPE_POSTGRESQL = "postgresql"; 34 | public static final String DB_TYPE_SQLITE = "sqlite"; 35 | public static final String DB_TYPE_H2 = "h2"; 36 | public static final String DB_TYPE_DM = "dm"; 37 | public static final String DB_TYPE_KINGBASE = "kingbase"; 38 | public static final String DB_TYPE_DB2 = "db2"; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/DbConfigManageAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.constant.MessageConstants; 4 | import com.chen.utils.SoarYamlUtil; 5 | import com.intellij.openapi.actionSystem.AnAction; 6 | import com.intellij.openapi.actionSystem.AnActionEvent; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.ui.Messages; 9 | import org.jetbrains.annotations.NotNull; 10 | import static com.chen.constant.MessageConstants.*; 11 | import static com.chen.utils.DbConfigUtil.promptUserInputWithDbType; 12 | import static com.chen.utils.DbConfigUtil.saveToCache; 13 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 14 | 15 | /** 16 | * @author czh 17 | * @version 1.0 18 | * @description: 数据库配置管理操作类 19 | * @date 2025/6/14 10:58 20 | */ 21 | public class DbConfigManageAction extends AnAction { 22 | 23 | @Override 24 | public void actionPerformed(@NotNull AnActionEvent element) { 25 | Project project = element.getProject(); 26 | if (project == null) { 27 | return; 28 | } 29 | promptUserInputWithDbType(element.getProject(), config -> { 30 | try { 31 | if (!testConnection(config)) { 32 | Messages.showErrorDialog(element.getProject(), 33 | MessageConstants.SQL_ERROR_CONNECTION_FAIL, 34 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 35 | return; 36 | } 37 | } catch (Exception ex) { 38 | Messages.showErrorDialog(element.getProject(), 39 | "数据库连接异常:" + ex.getMessage(), 40 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 41 | return; 42 | } 43 | 44 | if (saveToCache(element.getProject(), config)) { 45 | Messages.showInfoMessage(CONFIG_SWITCH_SUCCESS_MESSAGE, 46 | CONFIG_SAVE_SUCCESS_TITLE 47 | ); 48 | } 49 | }); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/ViewRealSqlAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.dialog.ParamInputDialog; 4 | import com.chen.dialog.SqlPreviewDialog; 5 | import com.chen.utils.SqlParamUtils; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.openapi.editor.SelectionModel; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.ui.Messages; 12 | 13 | import java.util.Collections; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | import static com.chen.constant.MessageConstants.DIALOG_TITLE; 18 | import static com.chen.constant.MessageConstants.MESSAGE_SELECT_SQL; 19 | 20 | /** 21 | * 主 Action 类:用于处理 SQL 选中、参数提取、弹窗调用 22 | */ 23 | public class ViewRealSqlAction extends AnAction { 24 | 25 | 26 | @Override 27 | public void actionPerformed(AnActionEvent e) { 28 | Project project = e.getProject(); 29 | Editor editor = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR); 30 | if (project == null || editor == null) return; 31 | 32 | // 获取 SQL 文本 33 | SelectionModel selectionModel = editor.getSelectionModel(); 34 | String sql = selectionModel.getSelectedText(); 35 | if (sql == null || sql.trim().isEmpty()) { 36 | Messages.showWarningDialog(project, MESSAGE_SELECT_SQL, DIALOG_TITLE); 37 | return; 38 | } 39 | 40 | // 解析 SQL 中的参数 41 | Set params = SqlParamUtils.extractParams(sql); 42 | 43 | Map paramValues = Collections.emptyMap(); 44 | if (!params.isEmpty()) { 45 | // 存在参数,弹参数输入框 46 | ParamInputDialog paramDialog = new ParamInputDialog(params, project); 47 | if (!paramDialog.showAndGet()) return; 48 | paramValues = paramDialog.getParamValues(); 49 | } 50 | 51 | // 生成预览 SQL 52 | String resultSql = SqlParamUtils.buildFinalSql(sql, paramValues); 53 | 54 | // 弹出预览窗口 55 | SqlPreviewDialog previewDialog = new SqlPreviewDialog(project, resultSql); 56 | previewDialog.showAndGet(); 57 | } 58 | } -------------------------------------------------------------------------------- /src/main/java/com/chen/action/DbConfigAddAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.constant.MessageConstants; 4 | import com.intellij.openapi.actionSystem.AnAction; 5 | import com.intellij.openapi.actionSystem.AnActionEvent; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.ui.Messages; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | import java.nio.file.Path; 11 | import java.nio.file.Paths; 12 | 13 | import static com.chen.constant.FileConstant.CONFIG_PATH; 14 | import static com.chen.constant.MessageConstants.*; 15 | import static com.chen.utils.DbConfigUtil.*; 16 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 17 | 18 | /** 19 | * @author czh 20 | * @version 1.0 21 | * @description: 数据库配置添加操作类 22 | * @date 2025/6/14 10:23 23 | */ 24 | public class DbConfigAddAction extends AnAction { 25 | 26 | @Override 27 | public void actionPerformed(@NotNull AnActionEvent element) { 28 | Project project = element.getProject(); 29 | if (project == null) { 30 | return; 31 | } 32 | promptUserAdd(element.getProject(), config -> { 33 | try { 34 | if (!testConnection(config)) { 35 | Messages.showErrorDialog(element.getProject(), 36 | MessageConstants.SQL_ERROR_CONNECTION_FAIL, 37 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 38 | return; 39 | } 40 | } catch (Exception ex) { 41 | Messages.showErrorDialog(element.getProject(), 42 | "数据库连接异常:" + ex.getMessage(), 43 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 44 | return; 45 | } 46 | if (saveToCache(element.getProject(), config)) { 47 | Path path = Paths.get(element.getProject().getBasePath(), CONFIG_PATH); 48 | Messages.showInfoMessage( 49 | CONFIG_SAVE_SUCCESS_MESSAGE_PREFIX + path.toString() + CONFIG_SAVE_SUCCESS_MESSAGE_SUFFIX, 50 | CONFIG_SAVE_SUCCESS_TITLE 51 | ); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/main/java/com/chen/dialog/ParamInputDialog.java: -------------------------------------------------------------------------------- 1 | package com.chen.dialog; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.ui.DialogWrapper; 5 | import org.jetbrains.annotations.Nullable; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.util.Collection; 10 | import java.util.LinkedHashMap; 11 | import java.util.Map; 12 | 13 | import static com.chen.constant.MessageConstants.DIALOG_TITLE; 14 | import static com.chen.constant.MessageConstants.PARAM_OPTIONAL_LABEL; 15 | 16 | /** 17 | * 参数填写对话框。支持多参数输入,所有参数均为选填 18 | */ 19 | public class ParamInputDialog extends DialogWrapper { 20 | // 常量 21 | 22 | private final Map fields = new LinkedHashMap<>(); 23 | private final JPanel panel; 24 | 25 | /** 26 | * 构造方法 27 | * @param params 需要填写的参数名集合 28 | * @param project IDEA Project 29 | */ 30 | public ParamInputDialog(Collection params, Project project) { 31 | super(project, true); 32 | setTitle("填写SQL参数"); 33 | panel = new JPanel(new GridBagLayout()); 34 | GridBagConstraints gbc = new GridBagConstraints(); 35 | gbc.insets = new Insets(6, 12, 6, 12); 36 | gbc.gridx = 0; 37 | gbc.gridy = 0; 38 | gbc.anchor = GridBagConstraints.EAST; 39 | 40 | // 动态生成每一个参数输入框 41 | for (String p : params) { 42 | JLabel label = new JLabel(p); 43 | JTextField tf = new JTextField(30); 44 | fields.put(p, tf); 45 | panel.add(label, gbc); 46 | gbc.gridx++; 47 | gbc.anchor = GridBagConstraints.WEST; 48 | panel.add(tf, gbc); 49 | gbc.gridx = 0; 50 | gbc.gridy++; 51 | gbc.anchor = GridBagConstraints.EAST; 52 | } 53 | init(); 54 | setResizable(true); 55 | } 56 | 57 | @Nullable 58 | @Override 59 | protected JComponent createCenterPanel() { 60 | return panel; 61 | } 62 | 63 | /** 64 | * 获取所有参数填写结果 65 | */ 66 | public Map getParamValues() { 67 | Map map = new LinkedHashMap<>(); 68 | for (Map.Entry e : fields.entrySet()) { 69 | map.put(e.getKey(), e.getValue().getText()); 70 | } 71 | return map; 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/com/chen/entity/DbConfig.java: -------------------------------------------------------------------------------- 1 | package com.chen.entity; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * 数据库配置信息类 7 | * 包含数据库的连接地址、用户名和密码 8 | * 用于数据库连接的初始化配置 9 | * 10 | * @author czh 11 | * @version 1.0 12 | * @date 2025/6/12 14:45 13 | */ 14 | public class DbConfig { 15 | 16 | // 数据库连接地址 17 | private String url; 18 | 19 | // 数据库用户名 20 | private String username; 21 | 22 | // 数据库密码 23 | private String password; 24 | 25 | public DbConfig() { 26 | } 27 | 28 | /** 29 | * 构造方法,初始化数据库配置 30 | * 31 | * @param url 数据库连接地址 32 | * @param username 数据库用户名 33 | * @param password 数据库密码 34 | */ 35 | public DbConfig(String url, String username, String password) { 36 | this.url = url; 37 | this.username = username; 38 | this.password = password; 39 | } 40 | 41 | /** 42 | * 获取数据库连接地址 43 | * 44 | * @return url 45 | */ 46 | public String getUrl() { 47 | return url; 48 | } 49 | 50 | /** 51 | * 设置数据库连接地址 52 | * 53 | * @param url 数据库地址 54 | */ 55 | public void setUrl(String url) { 56 | this.url = url; 57 | } 58 | 59 | /** 60 | * 获取数据库用户名 61 | * 62 | * @return 用户名 63 | */ 64 | public String getUsername() { 65 | return username; 66 | } 67 | 68 | /** 69 | * 设置数据库用户名 70 | * 71 | * @param username 用户名 72 | */ 73 | public void setUsername(String username) { 74 | this.username = username; 75 | } 76 | 77 | /** 78 | * 获取数据库密码 79 | * 80 | * @return 密码 81 | */ 82 | public String getPassword() { 83 | return password; 84 | } 85 | 86 | /** 87 | * 设置数据库密码 88 | * 89 | * @param password 密码 90 | */ 91 | public void setPassword(String password) { 92 | this.password = password; 93 | } 94 | 95 | @Override 96 | public boolean equals(Object o) { 97 | if (this == o) return true; 98 | if (o == null || getClass() != o.getClass()) return false; 99 | DbConfig dbConfig = (DbConfig) o; 100 | return Objects.equals(url, dbConfig.url) && Objects.equals(username, dbConfig.username) && Objects.equals(password, dbConfig.password); 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return Objects.hash(url, username, password); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /.intellijPlatform/localPlatformArtifacts/IU-243.25659.39/bundledPlugin-com.intellij.java-IU-243.25659.39.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.intellijPlatform/localPlatformArtifacts/IU-243.23654.117/bundledPlugin-com.intellij.java-IU-243.23654.117.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PrettySQL 多功能插件 2 | 3 | >PrettySQL 是一款基于 IntelliJ IDEA 的轻量级插件,致力于提升 SQL 开发体验,集成了 SQL 格式美化、高亮表结构提示、执行报告分析快速查看等功能,为日常开发中频繁处理 SQL 的用户提供便捷、清晰、可视化的开发辅助。 4 | 5 | [📚 文档中心](http://czh.znunwm.top/) | [🐛 反馈问题](https://github.com/SiYuan-2002/PrettySQL/issues) | 🌐 [English](https://github.com/SiYuan-2002/PrettySQL/blob/master/README_EN.md) 6 | 7 | ``` 8 | 9 | 项目基本保持每日更新,右上随手点个 🌟 Star 关注,这样才有持续下去的动力,谢谢~ 10 | 11 | ``` 12 | # 功能列表 13 | - SQL语句格式美化 14 | - 表结构悬浮提示 15 | - 支持多数据源 16 | - SQL语法检查 17 | - SQL执行计划分析 18 | - SQL检查报告 19 | - ER图 20 | 21 | 更多功能,请到[文档中心](http://czh.znunwm.top/)的项目主页进行了解。 22 | 23 | 24 | ### 2.1 引入PrettySQL插件(暂时不支持IntelliJ IDEA 2022.2.2之前版本) 25 | 26 | 在 IntelliJ IDEA 中打开插件市场,搜索 "PrettySQL" 安装 如果要离线安装请点击👉[离线手动安装](https://github.com/SiYuan-2002/PrettySQL/releases/tag/1.8)。 27 | 28 | 1.选择xml文件里的sql语句右键格式化SQL即可(支持选中xml标签) 29 | 30 | 2.如果自带yml配置文件默认读取这里,如果没有则右键弹窗数据库配置源自行添加 31 | 32 | 3.鼠标放到要查看表结构的表名称,悬浮即可出现 33 | 34 | 4.选择xml文件里的sql语句右键SQL检查(DML事务不会提交) 35 | 36 | 5.全面兼容mybatis xml语法自动翻译成真实SQL 37 | 38 | 39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 |
47 | 48 | ### 🎉 加入我们 49 | PrettySQL还在持续更新中,本项目欢迎您的参与,共同维护,逐步完善,将项目做得更强。项目仅供学习和个人使用,禁止任何商业用途和再次上架,如需商业授权或合作,请联系作者。 50 | 51 | 如果你想加入我们,可以多提供一些好的建议或者提交 pr,我们将会非常乐意接受您的建议和意见。 52 | 53 | 54 | 55 | 如果您觉得这个项目对您有帮助,可以帮作者买杯饮料鼓励鼓励! 56 | 57 |
58 | 59 |
60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/SqlCheckUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.constant.SqlConstants; 4 | import com.chen.entity.DbConfig; 5 | import net.sf.jsqlparser.JSQLParserException; 6 | import net.sf.jsqlparser.parser.CCJSqlParserUtil; 7 | 8 | import java.sql.Connection; 9 | import java.sql.Statement; 10 | 11 | /** 12 | * SQL 语句检查工具类。 13 | *

14 | * 提供对 SQL 执行安全性与语法合法性的静态校验功能,适用于开发工具、在线 SQL 编辑器等场景。 15 | * 包括: 16 | * - 通过回滚事务方式检测 SQL 是否能成功执行; 17 | * - 语法层面的静态检查; 18 | * - 检测是否存在无 WHERE 条件的危险 DELETE/UPDATE 操作。 19 | *

20 | */ 21 | public class SqlCheckUtil { 22 | 23 | /** 24 | * 尝试执行 SQL,并立即回滚,用于检测 SQL 是否能被数据库正常解析和执行。 25 | *

26 | * 注意:此方法不会真正修改数据库内容,执行后会立刻回滚事务。 27 | * 适用于检测 INSERT、UPDATE、DELETE 等 DML 语句是否可执行。 28 | *

29 | * 30 | * @param config 数据库连接配置 31 | * @param sql 要检测的 SQL 语句 32 | * @return 若无异常返回 null,若出错返回错误前缀加具体错误信息 33 | */ 34 | public static String checkSQLWithRollback(DbConfig config, String sql) { 35 | try (Connection conn = DataSourceManager.getDataSource(config).getConnection(); 36 | Statement stmt = conn.createStatement()) { 37 | 38 | conn.setAutoCommit(false); // 开启事务 39 | stmt.execute(sql); // 尝试执行 SQL 40 | conn.rollback(); // 执行后立即回滚 41 | return null; 42 | } catch (Exception e) { 43 | return SqlConstants.ERROR_SQL_EXECUTE_PREFIX + e.getMessage(); 44 | } 45 | } 46 | 47 | /** 48 | * 使用 JSqlParser 对 SQL 进行语法解析检查,仅验证语法是否合法。 49 | *

50 | * 已被标记为废弃,推荐使用真实数据库执行+回滚的方式校验。 51 | *

52 | * 53 | * @param sql 要检查的 SQL 字符串 54 | * @return 若语法正确返回 null,错误则返回错误信息 55 | */ 56 | @Deprecated 57 | private static String checkSyntaxOnly(String sql) { 58 | try { 59 | CCJSqlParserUtil.parse(sql); 60 | return null; 61 | } catch (JSQLParserException e) { 62 | return "SQL语法错误: " + e.getMessage(); 63 | } 64 | } 65 | 66 | /** 67 | * 检查是否存在危险 SQL,例如没有 WHERE 子句的 DELETE 或 UPDATE。 68 | *

69 | * 示例: 70 | * DELETE FROM user → ⚠️ 警告 71 | * UPDATE user SET name = 'x' → ⚠️ 警告 72 | *

73 | * 74 | * @param sql SQL 语句 75 | * @return 若是危险语句返回提示信息,否则返回 null 76 | */ 77 | public static String checkDangerous(String sql) { 78 | String lower = sql.trim().toLowerCase(); 79 | if ((lower.startsWith(SqlConstants.SQL_DELETE) || lower.startsWith(SqlConstants.SQL_UPDATE)) 80 | && !lower.contains(SqlConstants.SQL_WHERE)) { 81 | return SqlConstants.WARN_UNSAFE_DELETE_UPDATE; 82 | } 83 | return null; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/ColumnMetaUtils.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.entity.ColumnMeta; 4 | import com.chen.entity.DbConfig; 5 | import com.chen.entity.TableMeta; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.function.BiFunction; 10 | 11 | import static com.chen.constant.DataSourceConstants.*; 12 | 13 | /** 14 | * 数据库字段元数据工具类,用于根据数据库类型动态获取表字段信息。 15 | * 支持 Oracle、MySQL、SQL Server 等数据库类型。 16 | * 使用策略模式(Map + BiFunction)来优雅地分发处理逻辑。 17 | * 18 | * @author czh 19 | * @version 1.0 20 | * @since 2025/6/17 21 | */ 22 | public class ColumnMetaUtils { 23 | 24 | /** 25 | * 数据库类型到字段提取方法的映射。 26 | * Key 为数据库类型常量,Value 为提取逻辑函数。 27 | */ 28 | private static final Map> DB_TYPE_TO_HANDLER = Map.of( 29 | DB_TYPE_MYSQL, ColumnMetaUtils::getTableColumnsFromMySQL, 30 | DB_TYPE_ORACLE, ColumnMetaUtils::getTableColumnsFromOracle, 31 | DB_TYPE_SQLSERVER, ColumnMetaUtils::getTableColumnsFromSqlServer 32 | ); 33 | 34 | /** 35 | * 根据数据库配置和表名获取字段元数据列表。 36 | * 会根据数据库类型自动调用对应的处理方法。 37 | * 38 | * @param dbConfig 数据库连接配置(包含 URL、用户名、密码等) 39 | * @param tableName 表名 40 | * @return 字段元数据列表 41 | * @throws RuntimeException 如果不支持该数据库类型 42 | */ 43 | public static TableMeta getTableColumns(DbConfig dbConfig, String tableName) { 44 | String dbType = DbConfigUtil.parseDbType(dbConfig.getUrl()); 45 | BiFunction handler = DB_TYPE_TO_HANDLER.get(dbType); 46 | if (handler == null) { 47 | throw new RuntimeException("不支持的数据库类型: " + dbType); 48 | } 49 | return handler.apply(dbConfig, tableName); 50 | } 51 | 52 | /** 53 | * 获取 Oracle 数据库表字段元数据。 54 | * 55 | * @param dbConfig 数据库连接配置 56 | * @param tableName 表名 57 | * @return 字段元数据列表 58 | */ 59 | private static TableMeta getTableColumnsFromOracle(DbConfig dbConfig, String tableName) { 60 | return JdbcTableInfoUtil.getTableColumnsFromOracle(dbConfig, tableName); 61 | } 62 | 63 | /** 64 | * 获取 MySQL 数据库表字段元数据。 65 | * 66 | * @param dbConfig 数据库连接配置 67 | * @param tableName 表名 68 | * @return 字段元数据列表 69 | */ 70 | private static TableMeta getTableColumnsFromMySQL(DbConfig dbConfig, String tableName) { 71 | return JdbcTableInfoUtil.getTableMetaFromMySQL(dbConfig, tableName); 72 | } 73 | 74 | /** 75 | * 获取 SQL Server 数据库表字段元数据。 76 | * 77 | * @param dbConfig 数据库连接配置 78 | * @param tableName 表名 79 | * @return 字段元数据列表 80 | */ 81 | private static TableMeta getTableColumnsFromSqlServer(DbConfig dbConfig, String tableName) { 82 | return JdbcTableInfoUtil.getTableColumnsFromSqlServer(dbConfig, tableName); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/PrettySqlAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.entity.DbConfig; 4 | import com.chen.utils.SqlFormatUtil; 5 | import com.intellij.openapi.actionSystem.AnAction; 6 | import com.intellij.openapi.actionSystem.AnActionEvent; 7 | import com.intellij.openapi.actionSystem.CommonDataKeys; 8 | import com.intellij.openapi.command.WriteCommandAction; 9 | import com.intellij.openapi.editor.CaretModel; 10 | import com.intellij.openapi.editor.Editor; 11 | import com.intellij.openapi.editor.SelectionModel; 12 | import com.intellij.openapi.project.Project; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | import java.util.Arrays; 16 | import java.util.stream.Collectors; 17 | 18 | import static com.chen.utils.DbConfigUtil.loadFromCache; 19 | 20 | 21 | /** 22 | * SQL 格式化操作类 23 | * 当用户选中 SQL 文本时,自动进行美化格式化,并替换选中内容 24 | * 适用于 IntelliJ Platform 插件开发 25 | * 26 | * @author czh 27 | * @version 1.0 28 | * @date 2025/6/12 14:47 29 | */ 30 | public class PrettySqlAction extends AnAction { 31 | 32 | /** 33 | * 执行格式化操作的入口方法 34 | * 35 | * @param e 当前触发动作的事件对象 36 | */ 37 | @Override 38 | public void actionPerformed(@NotNull AnActionEvent e) { 39 | // 获取当前编辑器 40 | Editor editor = e.getData(CommonDataKeys.EDITOR); 41 | if (editor == null) return; 42 | 43 | // 获取当前选中内容 44 | SelectionModel selectionModel = editor.getSelectionModel(); 45 | String selectedText = selectionModel.getSelectedText(); 46 | if (selectedText == null || selectedText.isEmpty()) return; 47 | DbConfig dbConfig = loadFromCache(e.getProject()); 48 | // 使用工具类格式化 SQL 文本 49 | String formattedSql = SqlFormatUtil.formatSql(selectedText, dbConfig.getUrl().replaceFirst("^jdbc:(\\w+):.*", "$1")); 50 | 51 | // 设置缩进(这里是 11 个空格) 52 | String indent = " "; 53 | 54 | // 为格式化后的每一行添加统一缩进 55 | String indentedSql = Arrays.stream(formattedSql.split("\n")) 56 | .map(line -> indent + line) 57 | .collect(Collectors.joining("\n")); 58 | 59 | // 获取选中文本的起止位置 60 | int start = selectionModel.getSelectionStart(); 61 | int end = selectionModel.getSelectionEnd(); 62 | 63 | Project project = e.getProject(); 64 | 65 | // 执行替换操作,并支持撤销(IDE 操作必须在 WriteCommandAction 中执行) 66 | WriteCommandAction.runWriteCommandAction(project, () -> { 67 | editor.getDocument().replaceString(start, end, indentedSql); 68 | 69 | // 可选:将光标跳转到 SELECT 后的位置,便于继续编辑 70 | int newOffset = start + indentedSql.indexOf("SELECT"); 71 | if (newOffset >= start) { 72 | CaretModel caretModel = editor.getCaretModel(); 73 | caretModel.moveToOffset(newOffset + "SELECT".length() + 1); 74 | } 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/chen/constant/MessageConstants.java: -------------------------------------------------------------------------------- 1 | package com.chen.constant; 2 | 3 | /** 4 | * @author czh 5 | * @version 1.0 6 | * @description: 消息常量类 7 | * @date 2025/6/14 8:39 8 | */ 9 | public class MessageConstants { 10 | public static final String SQL_CHECK_TITLE = "SQL检查"; 11 | public static final String SQL_WARNING_EDITOR_EMPTY = "请在编辑器中选中或输入SQL语句"; 12 | public static final String SQL_WARNING_SQL_EMPTY = "未检测到SQL语句"; 13 | public static final String SQL_ERROR_NO_DB_CONFIG = "未配置数据库,无法执行SQL检查"; 14 | public static final String SQL_ERROR_CONNECTION_FAIL = "数据库连接失败,请检查配置是否正确。"; 15 | public static final String SQL_ERROR_TITLE_CONNECTION_FAIL = "连接失败"; 16 | public static final String SQL_ERROR_TITLE = "错误"; 17 | public static final String SQL_ERROR_SYNTAX = "SQL语法检查"; 18 | public static final String NOT_IN_SCOPE = "不在文件规定范围"; 19 | public static final String SQL_WARNING_DANGER = "SQL危险检测"; 20 | public static final String SQL_SUCCESS = "SQL语句语法正确"; 21 | public static final String SQL_SUCCESS_TITLE = "SQL检查"; 22 | public static final String CONFIG_SAVE_SUCCESS_MESSAGE_PREFIX = "数据库配置已保存到路径:\n"; 23 | public static final String CONFIG_SWITCH_SUCCESS_MESSAGE = "数据源切换成功"; 24 | public static final String CONFIG_SAVE_SUCCESS_MESSAGE_SUFFIX = "\n请重新将鼠标悬停以查看表结构。"; 25 | public static final String CONFIG_SAVE_SUCCESS_TITLE = "配置成功"; 26 | public static final String ERROR_PREFIX = "异常信息: "; 27 | public static final String DATASOURCE = "当前数据源:"; 28 | public static final String ERROR_NO_COLUMNS = "
未在数据库中找到该表结构或表无字段。"; 29 | public static final String CONFIG_SAVE_FAIL_PREFIX = "保存配置失败:"; 30 | public static final String CONFIG_SAVE_FAIL_TITLE = "保存错误"; 31 | public static final String DIALOG_TITLE = "执行计划分析"; 32 | public static final String ERROR_NO_EDITOR = "未选中任何 SQL,且编辑器内容为空"; 33 | public static final String ERROR_NO_DB_CONFIG = "未找到数据库配置,请先配置数据库连接"; 34 | public static final String ERROR_CONNECTION_FAIL = "数据库连接失败,请检查配置"; 35 | public static final String ERROR_CONNECTION_EXCEPTION_PREFIX = "数据库连接异常:"; 36 | public static final String WARN_NOT_SELECT = "执行计划只支持 SELECT 语句"; 37 | public static final String INFO_NO_RESULT = "执行计划无结果"; 38 | public static final String ERROR_ANALYZE_FAIL_PREFIX = "执行计划分析失败:"; 39 | public static final String DELETE_SUCCESS_MESSAGE = "数据源配置已删除"; 40 | public static final String DELETE_SUCCESS_TITLE = "删除成功"; 41 | public static final String NO_DB_CONFIG_HTML = "

请先配置或者新增数据库连接!

"; 42 | public static final String MESSAGE_SELECT_SQL = "请先选中一段SQL语句"; 43 | public static final String PARAM_OPTIONAL_LABEL = "(选填):"; 44 | public static final String BTN_SQL_CHECK = "SQL执行检查"; 45 | public static final String BTN_COPY = "复制"; 46 | public static final String DIALOG_RESULT = "执行计划分析"; 47 | public static final String COPY_SUCCESS_MSG = "已复制到剪贴板!"; 48 | 49 | } 50 | 51 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/SqlFormatUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.github.vertical_blank.sqlformatter.SqlFormatter; 4 | 5 | import java.util.Arrays; 6 | import java.util.LinkedHashMap; 7 | import java.util.Map; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | import java.util.stream.Collectors; 11 | 12 | /** 13 | * SQL 格式化工具类。 14 | *

15 | * 基于第三方库 {@code vertical-blank/sql-formatter},可根据指定方言对 SQL 字符串进行美化格式化, 16 | * 使 SQL 更易阅读,适用于开发工具、日志展示、SQL 编辑器等场景。 17 | * 如果传入方言无效或格式化失败,将原样返回 SQL 字符串。 18 | * @author czh 19 | * @version 1.0 20 | * @since 2025/6/12 21 | */ 22 | public class SqlFormatUtil { 23 | 24 | /** 25 | * 根据指定 SQL 方言对 SQL 字符串进行格式化美化处理。 26 | * 27 | * @param sql 原始 SQL 字符串(非空) 28 | * @param dialect SQL 方言名称,如 "mysql"、"postgresql"、"sql" 等 29 | * @return 格式化后的 SQL 字符串;如果格式化失败,返回原始 SQL 30 | */ 31 | public static String formatSql(String sql, String dialect) { 32 | try { 33 | // 1. 提取所有MyBatis标签(成对和自闭合)作为占位符,保存原始内容 34 | Map placeholderMap = new LinkedHashMap<>(); 35 | String regex = "<(\\w+)(\\s[^>]*)?>[\\s\\S]*?|<\\w+[^>]*/?>"; 36 | Pattern pattern = Pattern.compile(regex, Pattern.DOTALL); 37 | Matcher matcher = pattern.matcher(sql); 38 | 39 | int idx = 0; 40 | StringBuffer buffer = new StringBuffer(); 41 | while (matcher.find()) { 42 | String tag = matcher.group(); 43 | String key = "###PLACEHOLDER_" + idx++ + "###"; 44 | placeholderMap.put(key, tag); 45 | matcher.appendReplacement(buffer, key); 46 | } 47 | matcher.appendTail(buffer); 48 | 49 | // 2. 格式化没有标签的SQL部分 50 | String formatted = SqlFormatter.of(dialect).format(buffer.toString()); 51 | 52 | // 3. 逐个替换回原始标签 53 | for (Map.Entry entry : placeholderMap.entrySet()) { 54 | String key = entry.getKey(); 55 | String originalTag = entry.getValue(); 56 | formatted = formatted.replaceFirst(Pattern.quote(key), Matcher.quoteReplacement(originalTag)); 57 | } 58 | 59 | // 4. 标签缩进后处理 60 | String[] lines = formatted.split("\n"); 61 | StringBuilder result = new StringBuilder(); 62 | for (String line : lines) { 63 | String trim = line.trim(); 64 | if (trim.startsWith("<") && trim.endsWith(">")) { 65 | result.append(" ").append(trim).append("\n"); 66 | } else if(trim.equals("")) { 67 | result.append("\n"); 68 | } else { 69 | result.append(line).append("\n"); 70 | } 71 | } 72 | return result.toString(); 73 | 74 | } catch (Exception e) { 75 | e.printStackTrace(); 76 | return sql; 77 | } 78 | } 79 | 80 | 81 | 82 | 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/main/java/com/chen/entity/ColumnMeta.java: -------------------------------------------------------------------------------- 1 | package com.chen.entity; 2 | 3 | /** 4 | * 数据库字段元数据信息类 5 | * 用于描述表中的某一列,包括列名、类型、是否主键、是否索引、备注等信息 6 | * 可用于代码生成、数据库结构读取等场景 7 | * 8 | * @author czh 9 | * @version 2.0 10 | * @date 2025/6/19 11 | */ 12 | public class ColumnMeta { 13 | 14 | // 字段名称 15 | private String name; 16 | 17 | // 字段类型(如 VARCHAR、INT、DATE 等) 18 | private String type; 19 | 20 | // 是否为主键 21 | private boolean primaryKey; 22 | 23 | // 是否为索引字段(主键字段一定是索引,其他索引字段不一定是主键) 24 | private boolean index; 25 | 26 | // 字段备注(注释) 27 | private String remark; 28 | 29 | public ColumnMeta() { 30 | } 31 | 32 | /** 33 | * 构造方法,初始化字段元数据 34 | * 35 | * @param name 字段名 36 | * @param type 字段类型 37 | * @param primaryKey 是否为主键 38 | * @param index 是否为索引字段 39 | * @param remark 字段备注 40 | */ 41 | public ColumnMeta(String name, String type, boolean primaryKey, boolean index, String remark) { 42 | this.name = name; 43 | this.type = type; 44 | this.primaryKey = primaryKey; 45 | this.index = index; 46 | this.remark = remark; 47 | } 48 | 49 | /** 50 | * 获取字段名称 51 | * @return 字段名 52 | */ 53 | public String getName() { 54 | return name; 55 | } 56 | 57 | /** 58 | * 设置字段名称 59 | * @param name 字段名 60 | */ 61 | public void setName(String name) { 62 | this.name = name; 63 | } 64 | 65 | /** 66 | * 获取字段类型 67 | * @return 字段类型 68 | */ 69 | public String getType() { 70 | return type; 71 | } 72 | 73 | /** 74 | * 设置字段类型 75 | * @param type 字段类型 76 | */ 77 | public void setType(String type) { 78 | this.type = type; 79 | } 80 | 81 | /** 82 | * 是否为主键 83 | * @return true 是主键,false 不是 84 | */ 85 | public boolean isPrimaryKey() { 86 | return primaryKey; 87 | } 88 | 89 | /** 90 | * 设置是否为主键 91 | * @param primaryKey 是否主键 92 | */ 93 | public void setPrimaryKey(boolean primaryKey) { 94 | this.primaryKey = primaryKey; 95 | } 96 | 97 | /** 98 | * 是否为索引字段 99 | * @return true 是索引,false 不是 100 | */ 101 | public boolean isIndex() { 102 | return index; 103 | } 104 | 105 | /** 106 | * 设置是否为索引字段 107 | * @param index 是否索引字段 108 | */ 109 | public void setIndex(boolean index) { 110 | this.index = index; 111 | } 112 | 113 | /** 114 | * 获取字段备注 115 | * @return 字段备注 116 | */ 117 | public String getRemark() { 118 | return remark; 119 | } 120 | 121 | /** 122 | * 设置字段备注 123 | * @param remark 字段备注 124 | */ 125 | public void setRemark(String remark) { 126 | this.remark = remark; 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return "ColumnMeta{" + 132 | "name='" + name + '\'' + 133 | ", type='" + type + '\'' + 134 | ", primaryKey=" + primaryKey + 135 | ", index=" + index + 136 | ", remark='" + remark + '\'' + 137 | '}'; 138 | } 139 | } -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/DataSourceManager.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.constant.DataSourceConstants; 4 | import com.chen.entity.DbConfig; 5 | import com.zaxxer.hikari.HikariConfig; 6 | import com.zaxxer.hikari.HikariDataSource; 7 | 8 | import javax.sql.DataSource; 9 | import java.util.Map; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | import static com.chen.utils.DbConfigUtil.parseDbType; 13 | 14 | /** 15 | * 数据源管理类,用于统一管理数据库连接池实例。 16 | * 基于 HikariCP 实现数据源的创建、缓存与复用。 17 | * 支持 MySQL、Oracle、SQL Server 等主流数据库。 18 | */ 19 | public class DataSourceManager { 20 | 21 | /** 22 | * 数据库类型与对应 JDBC 驱动类名的映射。 23 | */ 24 | private static final Map DRIVER_MAP = Map.of( 25 | DataSourceConstants.DB_TYPE_MYSQL, DataSourceConstants.MYSQL_DRIVER, 26 | DataSourceConstants.DB_TYPE_ORACLE, DataSourceConstants.ORACLE_DRIVER, 27 | DataSourceConstants.DB_TYPE_SQLSERVER, DataSourceConstants.SQLSERVER_DRIVER, 28 | DataSourceConstants.DB_TYPE_POSTGRESQL, DataSourceConstants.POSTGRESQL_DRIVER 29 | ); 30 | 31 | /** 32 | * 数据源缓存池,key 为 DbConfig,value 为 HikariDataSource。 33 | * 使用 ConcurrentHashMap 保证线程安全。 34 | */ 35 | private static final Map cache = new ConcurrentHashMap<>(); 36 | 37 | /** 38 | * 获取指定配置的 DataSource,如果缓存中不存在则创建新的。 39 | * 40 | * @param config 数据库连接配置 41 | * @return 对应的数据源对象(HikariDataSource 实现) 42 | */ 43 | public static DataSource getDataSource(DbConfig config) { 44 | return cache.computeIfAbsent(config, cfg -> { 45 | loadJdbcDriver(cfg.getUrl()); 46 | return createHikariDataSource(cfg); 47 | }); 48 | } 49 | 50 | /** 51 | * 根据数据库 URL 加载对应的 JDBC 驱动类。 52 | * 53 | * @param url 数据库连接 URL 54 | * @throws RuntimeException 如果不支持该数据库或驱动加载失败 55 | */ 56 | private static void loadJdbcDriver(String url) { 57 | String dbType = parseDbType(url); 58 | String driverClass = DRIVER_MAP.get(dbType); 59 | if (driverClass == null) { 60 | throw new RuntimeException("不支持的数据库类型: " + dbType); 61 | } 62 | try { 63 | Class.forName(driverClass); 64 | } catch (ClassNotFoundException e) { 65 | throw new RuntimeException("JDBC驱动加载失败: " + e.getMessage(), e); 66 | } 67 | } 68 | 69 | /** 70 | * 创建一个新的 HikariCP 数据源实例。 71 | * 72 | * @param cfg 数据库连接配置 73 | * @return 初始化后的 HikariDataSource 实例 74 | */ 75 | private static HikariDataSource createHikariDataSource(DbConfig cfg) { 76 | HikariConfig hikariConfig = new HikariConfig(); 77 | hikariConfig.setJdbcUrl(cfg.getUrl()); 78 | hikariConfig.setUsername(cfg.getUsername()); 79 | hikariConfig.setPassword(cfg.getPassword()); 80 | hikariConfig.setMaximumPoolSize(DataSourceConstants.MAX_POOL_SIZE); // 最大连接数 81 | hikariConfig.setMinimumIdle(DataSourceConstants.MIN_IDLE); // 最小空闲连接数 82 | hikariConfig.setConnectionTimeout(DataSourceConstants.CONNECTION_TIMEOUT); // 获取连接超时时间 83 | hikariConfig.setIdleTimeout(DataSourceConstants.IDLE_TIMEOUT); // 空闲连接最大存活时间 84 | hikariConfig.setMaxLifetime(DataSourceConstants.MAX_LIFETIME); // 连接最大生命周期 85 | return new HikariDataSource(hikariConfig); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # PrettySQL - Powerful SQL Plugin for IntelliJ 2 | 3 | > PrettySQL is a lightweight IntelliJ plugin designed to enhance SQL development. It integrates features like SQL formatting, table structure tooltips, syntax checking, and execution plan analysis to provide a cleaner, more visual, and efficient SQL development experience. 4 | 5 | [📚 Documentation](http://czh.znunwm.top/) | [🐛 Report Issues](https://github.com/SiYuan-2002/PrettySQL/issues) | 🇨🇳 [简体中文](https://github.com/SiYuan-2002/PrettySQL/blob/master/README.md) 6 | 7 | 8 | ## 🔧 Features 9 | 10 | - Beautify SQL statements with a single click 11 | - Table structure tooltip when hovering over table names 12 | - Support for multiple data sources 13 | - SQL syntax checking 14 | - SQL execution plan analysis 15 | - SQL check report generation 16 | - ER diagram generation 17 | 18 | For more features, visit the [Documentation Center](http://czh.znunwm.top/). 19 | 20 | ## 🚀 Getting Started 21 | 22 | PrettySQL supports JetBrains IDEs like **IntelliJ IDEA**, **PyCharm**, **CLion**, **DataGrip**, etc. (Currently not compatible with versions earlier than IntelliJ IDEA 2022.2.2) 23 | 24 | ### Installation Steps 25 | 26 | 1. Open your JetBrains IDE and go to **File → Settings → Plugins** (or **Preferences** on macOS) 27 | 2. Search for **PrettySQL** in the plugin marketplace and click **Install** 28 | 3. Restart your IDE after installation 29 | 4. Right-click SQL inside XML files to format them (supports selected XML tags) 30 | 5. If a `yml` config file is present, it's used automatically. Otherwise, a popup window will guide you to add a data source 31 | 6. Hover over a table name to view its structure tooltip 32 | 7. Right-click SQL to check syntax and execution plan (DML will not be committed) 33 | 34 | ## 🖼️ Screenshots 35 | 36 |

37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | ## 🤝 Join Us 46 | 47 | PrettySQL is continuously evolving. Contributions are welcome — whether it's feedback, feature suggestions, or pull requests. 48 | 49 | **This project is for learning and personal use only. Commercial use and re-publishing (including uploading to any marketplace or app store) are strictly prohibited without explicit permission from the author.** 50 | 51 | If you're interested in joining or helping out, feel free to reach out with your ideas! 52 | 53 | --- 54 | 55 | ## ☕ Support the Author 56 | 57 | If you find PrettySQL helpful, consider buying a coffee to support continued development ❤️ 58 | 59 |
60 | 61 |
62 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/CheckSqlAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.constant.MessageConstants; 4 | import com.chen.entity.DbConfig; 5 | import com.chen.utils.SqlCheckUtil; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.editor.Editor; 9 | import com.intellij.openapi.editor.SelectionModel; 10 | import com.intellij.openapi.project.Project; 11 | import com.intellij.openapi.ui.Messages; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | import java.util.Optional; 15 | 16 | import static com.chen.utils.DbConfigUtil.*; 17 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 18 | 19 | /** 20 | * @author czh 21 | * @version 1.0 22 | * @description: SQL语法检查操作类 23 | * @date 2025/6/13 14:29 24 | */ 25 | 26 | public class CheckSqlAction extends AnAction { 27 | @Override 28 | public void actionPerformed(@NotNull AnActionEvent e) { 29 | Project project = e.getProject(); 30 | Editor editor = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR); 31 | 32 | if (editor == null) { 33 | Messages.showWarningDialog(project, 34 | MessageConstants.SQL_WARNING_EDITOR_EMPTY, 35 | MessageConstants.SQL_CHECK_TITLE); 36 | return; 37 | } 38 | 39 | SelectionModel selectionModel = editor.getSelectionModel(); 40 | String sql = selectionModel.getSelectedText(); 41 | if (sql == null || sql.trim().isEmpty()) { 42 | sql = editor.getDocument().getText(); 43 | } 44 | 45 | if (sql == null || sql.trim().isEmpty()) { 46 | Messages.showWarningDialog(project, 47 | MessageConstants.SQL_WARNING_SQL_EMPTY, 48 | MessageConstants.SQL_CHECK_TITLE); 49 | return; 50 | } 51 | 52 | Optional dbConfigOpt = Optional.ofNullable(loadFromCache(e.getProject())) 53 | .or(() -> Optional.ofNullable(tryLoadDbConfig(e.getProject()))) 54 | .or(() -> Optional.ofNullable(promptUserInputSync(e.getProject()))); 55 | 56 | if (!dbConfigOpt.isPresent()) { 57 | Messages.showErrorDialog(e.getProject(), 58 | MessageConstants.SQL_ERROR_NO_DB_CONFIG, 59 | MessageConstants.SQL_ERROR_TITLE); 60 | return; 61 | } 62 | 63 | DbConfig dbConfig = dbConfigOpt.get(); 64 | 65 | try { 66 | if (!testConnection(dbConfig)) { 67 | Messages.showErrorDialog(e.getProject(), 68 | MessageConstants.SQL_ERROR_CONNECTION_FAIL, 69 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 70 | return; 71 | } 72 | } catch (Exception ex) { 73 | Messages.showErrorDialog(e.getProject(), 74 | "数据库连接异常:" + ex.getMessage(), 75 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 76 | return; 77 | } 78 | 79 | 80 | // 语法检查 81 | String syntaxResult = SqlCheckUtil.checkSQLWithRollback(dbConfig, sql); 82 | if (syntaxResult != null) { 83 | Messages.showErrorDialog(project, 84 | syntaxResult, 85 | MessageConstants.SQL_ERROR_SYNTAX); 86 | return; 87 | } 88 | 89 | // 危险SQL检查 90 | String dangerResult = SqlCheckUtil.checkDangerous(sql); 91 | if (dangerResult != null) { 92 | Messages.showWarningDialog(project, 93 | dangerResult, 94 | MessageConstants.SQL_WARNING_DANGER); 95 | return; 96 | } 97 | 98 | Messages.showInfoMessage(project, 99 | MessageConstants.SQL_SUCCESS, 100 | MessageConstants.SQL_SUCCESS_TITLE); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/HtmlViewerUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.intellij.openapi.project.Project; 4 | import com.intellij.openapi.ui.Messages; 5 | import com.intellij.openapi.ui.popup.JBPopup; 6 | import com.intellij.openapi.ui.popup.JBPopupFactory; 7 | import groovyjarjarantlr4.v4.runtime.misc.Nullable; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | 12 | public class HtmlViewerUtil { 13 | 14 | /** 15 | * 弹出一个简单的 HTML 查看窗口 16 | * @param html HTML 字符串内容 17 | * @param title 窗口标题 18 | */ 19 | public static void showHtml(Project project, String html, String title, @Nullable String markdownReportHtml, boolean isReportPage) { 20 | JEditorPane editorPane = new JEditorPane("text/html", html); 21 | editorPane.setEditable(false); 22 | editorPane.setCaretPosition(0); 23 | 24 | JScrollPane scrollPane = new JScrollPane(editorPane); 25 | scrollPane.setPreferredSize(new Dimension(1500, 600)); 26 | scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 27 | scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 28 | 29 | JPanel panel = new JPanel(new BorderLayout()); 30 | panel.add(scrollPane, BorderLayout.CENTER); 31 | 32 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 33 | 34 | // 如果是展示原始HTML的弹窗,才显示“查看分析报告和建议”按钮 35 | if (!isReportPage && markdownReportHtml != null) { 36 | JButton viewReportBtn = new JButton("查看分析报告和建议"); 37 | viewReportBtn.addActionListener(e -> { 38 | showHtml(project, markdownReportHtml, "SQL分析报告", null, true); 39 | }); 40 | buttonPanel.add(viewReportBtn); 41 | } 42 | 43 | if (isReportPage) { 44 | JButton rewriteSQLBtn = new JButton("SQL重写"); 45 | rewriteSQLBtn.addActionListener(e -> { 46 | try { 47 | String rewriteSQL = SoarYamlUtil.rewriteSQL(project); 48 | Messages.showInfoMessage(project, rewriteSQL, "重写成功"); 49 | } catch (Exception ex) { 50 | throw new RuntimeException(ex); 51 | } 52 | }); 53 | buttonPanel.add(rewriteSQLBtn); 54 | 55 | JButton generateHtmlBtn = new JButton("生成 HTML"); 56 | generateHtmlBtn.addActionListener(e -> { 57 | try { 58 | SoarYamlUtil.downHtml(project); 59 | } catch (Exception ex) { 60 | throw new RuntimeException(ex); 61 | } 62 | }); 63 | buttonPanel.add(generateHtmlBtn); 64 | 65 | JButton generateMdBtn = new JButton("生成 MD"); 66 | generateMdBtn.addActionListener(e -> { 67 | try { 68 | SoarYamlUtil.downMD(project); 69 | } catch (Exception ex) { 70 | throw new RuntimeException(ex); 71 | } 72 | }); 73 | buttonPanel.add(generateMdBtn); 74 | } 75 | 76 | // 添加关闭按钮,必须点击它才能关闭 77 | JButton closeBtn = new JButton("关闭"); 78 | buttonPanel.add(closeBtn); 79 | 80 | panel.add(buttonPanel, BorderLayout.SOUTH); 81 | 82 | JBPopup popup = JBPopupFactory.getInstance() 83 | .createComponentPopupBuilder(panel, null) 84 | .setTitle(title) 85 | .setResizable(true) 86 | .setMovable(true) 87 | .setRequestFocus(true) 88 | // 禁止点击弹窗外部关闭 89 | .setCancelOnClickOutside(false) 90 | // 禁止按ESC关闭 91 | .setCancelOnWindowDeactivation(false) 92 | .createPopup(); 93 | 94 | // 点击关闭按钮时关闭弹窗 95 | closeBtn.addActionListener(e -> popup.cancel()); 96 | 97 | popup.showInFocusCenter(); 98 | } 99 | 100 | 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/chen/dialog/SqlPreviewDialog.java: -------------------------------------------------------------------------------- 1 | package com.chen.dialog; 2 | import com.chen.constant.MessageConstants; 3 | import com.chen.entity.DbConfig; 4 | import com.chen.utils.SqlCheckUtil; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.ui.DialogWrapper; 7 | import com.intellij.openapi.ui.Messages; 8 | import org.jetbrains.annotations.Nullable; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.awt.datatransfer.StringSelection; 13 | import java.util.Optional; 14 | 15 | import static com.chen.utils.DbConfigUtil.*; 16 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 17 | import static com.chen.constant.MessageConstants.*; 18 | /** 19 | * @author czh 20 | * @version 1.0 21 | * @description: SQL 预览窗口。含“SQL执行检查”“复制”“关闭”等功能按钮 22 | * @date 2025/6/25 14:35 23 | */ 24 | public class SqlPreviewDialog extends DialogWrapper { 25 | 26 | private final Project project; 27 | private final String sql; 28 | private JTextArea textArea; 29 | 30 | /** 31 | * 构造方法 32 | */ 33 | public SqlPreviewDialog(Project project, String sql) { 34 | super(project, true); 35 | this.project = project; 36 | this.sql = sql; 37 | setTitle("预览实际 SQL"); 38 | init(); 39 | } 40 | 41 | @Nullable 42 | @Override 43 | protected JComponent createCenterPanel() { 44 | textArea = new JTextArea(sql, Math.min(20, sql.split("\n").length + 1), 80); 45 | textArea.setLineWrap(true); 46 | textArea.setWrapStyleWord(true); 47 | textArea.setCaretPosition(0); 48 | textArea.setEditable(false); 49 | JScrollPane scrollPane = new JScrollPane(textArea); 50 | scrollPane.setPreferredSize(new Dimension(900, 400)); 51 | return scrollPane; 52 | } 53 | 54 | @Override 55 | protected Action[] createActions() { 56 | return new Action[]{ 57 | new AbstractAction(BTN_SQL_CHECK) { 58 | @Override 59 | public void actionPerformed(java.awt.event.ActionEvent e) { 60 | doSqlCheck(); 61 | } 62 | }, 63 | new AbstractAction(BTN_COPY) { 64 | @Override 65 | public void actionPerformed(java.awt.event.ActionEvent e) { 66 | StringSelection selection = new StringSelection(textArea.getText()); 67 | Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection); 68 | Messages.showInfoMessage(project, COPY_SUCCESS_MSG, BTN_COPY); 69 | } 70 | }, 71 | getOKAction() 72 | }; 73 | } 74 | 75 | /** 76 | * SQL执行检查逻辑,可根据实际需求扩展 77 | */ 78 | private void doSqlCheck() { 79 | if (sql == null || sql.trim().isEmpty()) { 80 | Messages.showWarningDialog(project, 81 | MessageConstants.SQL_WARNING_SQL_EMPTY, 82 | MessageConstants.SQL_CHECK_TITLE); 83 | return; 84 | } 85 | 86 | Optional dbConfigOpt = Optional.ofNullable(loadFromCache(project)) 87 | .or(() -> Optional.ofNullable(tryLoadDbConfig(project))) 88 | .or(() -> Optional.ofNullable(promptUserInputSync(project))); 89 | 90 | if (!dbConfigOpt.isPresent()) { 91 | Messages.showErrorDialog(project, 92 | MessageConstants.SQL_ERROR_NO_DB_CONFIG, 93 | MessageConstants.SQL_ERROR_TITLE); 94 | return; 95 | } 96 | 97 | DbConfig dbConfig = dbConfigOpt.get(); 98 | 99 | try { 100 | if (!testConnection(dbConfig)) { 101 | Messages.showErrorDialog(project, 102 | MessageConstants.SQL_ERROR_CONNECTION_FAIL, 103 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 104 | return; 105 | } 106 | } catch (Exception ex) { 107 | Messages.showErrorDialog(project, 108 | "数据库连接异常:" + ex.getMessage(), 109 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 110 | return; 111 | } 112 | 113 | // 语法检查 114 | String syntaxResult = SqlCheckUtil.checkSQLWithRollback(dbConfig, sql); 115 | if (syntaxResult != null) { 116 | Messages.showErrorDialog(project, 117 | syntaxResult, 118 | MessageConstants.SQL_ERROR_SYNTAX); 119 | return; 120 | } 121 | 122 | // 危险SQL检查 123 | String dangerResult = SqlCheckUtil.checkDangerous(sql); 124 | if (dangerResult != null) { 125 | Messages.showWarningDialog(project, 126 | dangerResult, 127 | MessageConstants.SQL_WARNING_DANGER); 128 | return; 129 | } 130 | 131 | Messages.showInfoMessage(project, 132 | MessageConstants.SQL_SUCCESS, 133 | MessageConstants.SQL_SUCCESS_TITLE); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.chen.PrettySQL 5 | 6 | 8 | PrettySQL 9 | 10 | 11 | 陈思源 12 | 13 | 16 | 19 | 20 | 21 | 23 | com.intellij.modules.platform 24 | 25 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | 62 | 68 | 69 | 70 | 71 | 72 | 76 | 77 | 78 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | 89 | 93 | 94 | 95 | 96 | 97 | 101 | 102 | 103 | 104 | 105 | 106 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/SqlTableDocumentationProvider.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.constant.MessageConstants; 4 | import com.chen.entity.ColumnMeta; 5 | import com.chen.entity.DbConfig; 6 | import com.chen.entity.TableMeta; 7 | import com.chen.utils.JdbcTableInfoUtil; 8 | import com.intellij.lang.documentation.AbstractDocumentationProvider; 9 | import com.intellij.lang.documentation.DocumentationProvider; 10 | import com.intellij.openapi.ui.Messages; 11 | import com.intellij.openapi.vfs.VirtualFile; 12 | import com.intellij.psi.PsiElement; 13 | import com.intellij.psi.PsiFile; 14 | import com.intellij.psi.xml.XmlText; 15 | import com.intellij.psi.xml.XmlToken; 16 | import org.jetbrains.annotations.Nullable; 17 | 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | 21 | import java.util.List; 22 | 23 | import java.util.Optional; 24 | import java.util.regex.Matcher; 25 | import java.util.regex.Pattern; 26 | 27 | import static com.chen.constant.FileConstant.CONFIG_PATH; 28 | import static com.chen.constant.FileConstant.XML; 29 | import static com.chen.constant.MessageConstants.*; 30 | import static com.chen.utils.DbConfigUtil.*; 31 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 32 | 33 | /** 34 | * SQL 表结构文档展示提供器:支持在 IntelliJ 中对 SQL 中的表名悬停显示结构 35 | * 自动从 application.yml / application-dev.yml 中提取数据库连接配置并通过 JDBC 获取表字段元数据 36 | */ 37 | public class SqlTableDocumentationProvider extends AbstractDocumentationProvider { 38 | 39 | 40 | /** 41 | * 悬停展示文档:从 PSI 中识别表名,读取数据库连接并生成 HTML 表格 42 | */ 43 | @Override 44 | public @Nullable String generateDoc(PsiElement element, @Nullable PsiElement originalElement) { 45 | String extension = Optional.ofNullable(element.getContainingFile()) 46 | .map(PsiFile::getVirtualFile) 47 | .map(VirtualFile::getExtension) 48 | .orElse(""); 49 | 50 | if (!XML.equalsIgnoreCase(extension)) { 51 | return NOT_IN_SCOPE; 52 | } 53 | 54 | 55 | String tableName = extractTableName(element); 56 | if (tableName == null) { 57 | return null; 58 | } 59 | 60 | DbConfig dbConfig = loadFromCache(element.getProject()); 61 | 62 | if (dbConfig == null) { 63 | dbConfig = tryLoadDbConfig(element.getProject()); 64 | } 65 | 66 | if (dbConfig == null) { 67 | promptUserInput(element.getProject(), config -> { 68 | try { 69 | if (!testConnection(config)) { 70 | Messages.showErrorDialog(element.getProject(), 71 | MessageConstants.SQL_ERROR_CONNECTION_FAIL, 72 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 73 | return; 74 | } 75 | } catch (Exception ex) { 76 | Messages.showErrorDialog(element.getProject(), 77 | "数据库连接异常:" + ex.getMessage(), 78 | MessageConstants.SQL_ERROR_TITLE_CONNECTION_FAIL); 79 | return; 80 | } 81 | 82 | if (saveToCache(element.getProject(), config)) { 83 | Path path = Paths.get(element.getProject().getBasePath(), CONFIG_PATH); 84 | Messages.showInfoMessage( 85 | CONFIG_SAVE_SUCCESS_MESSAGE_PREFIX + path.toString() + CONFIG_SAVE_SUCCESS_MESSAGE_SUFFIX, 86 | CONFIG_SAVE_SUCCESS_TITLE 87 | ); 88 | } 89 | }); 90 | } 91 | 92 | List columns; 93 | String tableRemark = null; 94 | try { 95 | TableMeta tableColumns = JdbcTableInfoUtil.getTableColumns(dbConfig, tableName); 96 | columns = tableColumns.getColumns(); 97 | tableRemark = tableColumns.getTableComment(); 98 | } catch (Exception e) { 99 | return ERROR_PREFIX + tableName + DATASOURCE + dbConfig.getUrl() + e.getMessage(); 100 | } 101 | 102 | if (columns == null || columns.isEmpty()) { 103 | return ERROR_PREFIX + tableName + DATASOURCE + dbConfig.getUrl() + ERROR_NO_COLUMNS; 104 | } 105 | 106 | return buildHtmlTable(tableName,tableRemark, columns); 107 | } 108 | 109 | 110 | /** 111 | * 构建表结构的 HTML 表格展示 112 | */ 113 | private String buildHtmlTable(String tableName, String tableRemark,List columns) { 114 | StringBuilder html = new StringBuilder(); 115 | html.append(""); 124 | 125 | html.append("表名: ") 126 | .append(tableName) 127 | .append(tableRemark != null && !tableRemark.isBlank() ? "(" + tableRemark + ")" : "") 128 | .append("
"); 129 | html.append("") 130 | .append("") 131 | .append("") 132 | .append("") 133 | .append("") 134 | .append("") 135 | .append(""); 136 | 137 | for (ColumnMeta col : columns) { 138 | html.append("") 139 | .append("") 140 | .append("") 141 | .append("") 142 | .append(""); 143 | } 144 | 145 | html.append("
字段名称类型备注主键索引
").append(col.getName()).append("").append(col.getType()).append("").append(col.getRemark() == null ? "" : col.getRemark()).append("").append(col.isPrimaryKey() ? "YES" : "").append("").append(col.isIndex() ? "YES" : "").append("
"); 146 | return html.toString(); 147 | } 148 | 149 | /** 150 | * 提取 SQL 中的表名,适配 MyBatis XML 或 SQL 字符串 151 | */ 152 | private @Nullable String extractTableName(PsiElement element) { 153 | if (element == null) return null; 154 | String text = element.getText(); 155 | if (text == null || !text.matches("[a-zA-Z_][a-zA-Z0-9_]*")) return null; 156 | 157 | PsiElement parent = element.getParent(); 158 | 159 | // 支持 from/join 关键字场景 160 | if (parent != null && parent.getText().matches("(?i).*\\b(from|join)\\b.*" + text + ".*")) { 161 | return text; 162 | } 163 | 164 | // XML/SQL 中的 token 内容匹配 165 | if (element instanceof XmlToken || element instanceof XmlText) { 166 | Pattern pattern = Pattern.compile("\\b(from|join)\\s+([a-zA-Z_][a-zA-Z0-9_]*)", Pattern.CASE_INSENSITIVE); 167 | Matcher matcher = pattern.matcher(text); 168 | if (matcher.find()) { 169 | return matcher.group(2); 170 | } 171 | } 172 | 173 | return text; 174 | } 175 | 176 | 177 | } 178 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/ExplainSqlAction.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.dialog.ParamInputDialog; 4 | import com.chen.entity.DbConfig; 5 | import com.chen.utils.HtmlViewerUtil; 6 | import com.chen.utils.JdbcTableInfoUtil; 7 | import com.chen.utils.SoarYamlUtil; 8 | import com.chen.utils.SqlParamUtils; 9 | import com.intellij.openapi.actionSystem.AnAction; 10 | import com.intellij.openapi.actionSystem.AnActionEvent; 11 | import com.intellij.openapi.editor.Editor; 12 | import com.intellij.openapi.editor.SelectionModel; 13 | import com.intellij.openapi.project.Project; 14 | import com.intellij.openapi.ui.Messages; 15 | import org.jetbrains.annotations.NotNull; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Optional; 19 | 20 | import java.util.*; 21 | 22 | import static com.chen.constant.MessageConstants.*; 23 | import static com.chen.utils.DbConfigUtil.*; 24 | import static com.chen.utils.JdbcTableInfoUtil.testConnection; 25 | import static com.chen.utils.SoarYamlUtil.runSoarFixed; 26 | import static com.chen.utils.SoarYamlUtil.writeSqlToIdea; 27 | 28 | /** 29 | * 执行计划分析操作类 30 | * 模仿 CheckSqlAction,实现 EXPLAIN 执行计划展示 31 | */ 32 | public class ExplainSqlAction extends AnAction { 33 | 34 | @Override 35 | public void actionPerformed(@NotNull AnActionEvent e) { 36 | Project project = e.getProject(); 37 | Editor editor = e.getData(com.intellij.openapi.actionSystem.CommonDataKeys.EDITOR); 38 | 39 | if (editor == null) { 40 | Messages.showWarningDialog(project, 41 | ERROR_NO_EDITOR, 42 | DIALOG_TITLE); 43 | return; 44 | } 45 | 46 | // 获取 SQL 文本 47 | SelectionModel selectionModel = editor.getSelectionModel(); 48 | String sqlTmp = selectionModel.getSelectedText(); 49 | if (sqlTmp == null || sqlTmp.trim().isEmpty()) { 50 | Messages.showWarningDialog(project, MESSAGE_SELECT_SQL, DIALOG_TITLE); 51 | return; 52 | } 53 | 54 | // 解析 SQL 中的参数 55 | Set params = SqlParamUtils.extractParams(sqlTmp); 56 | 57 | Map paramValues = Collections.emptyMap(); 58 | if (!params.isEmpty()) { 59 | // 存在参数,弹参数输入框 60 | ParamInputDialog paramDialog = new ParamInputDialog(params, project); 61 | if (!paramDialog.showAndGet()) return; 62 | paramValues = paramDialog.getParamValues(); 63 | } 64 | 65 | // 生成预览 SQL 66 | String sql = SqlParamUtils.buildFinalSql(sqlTmp, paramValues); 67 | if (sql == null || sql.trim().isEmpty()) { 68 | sql = editor.getDocument().getText(); 69 | } 70 | 71 | if (sql == null || sql.trim().isEmpty()) { 72 | Messages.showWarningDialog(project, 73 | ERROR_NO_EDITOR, 74 | DIALOG_TITLE); 75 | return; 76 | } 77 | 78 | Optional dbConfigOpt = Optional.ofNullable(loadFromCache(project)) 79 | .or(() -> Optional.ofNullable(tryLoadDbConfig(project))) 80 | .or(() -> Optional.ofNullable(promptUserInputSync(project))); 81 | 82 | if (!dbConfigOpt.isPresent()) { 83 | Messages.showErrorDialog(project, 84 | ERROR_NO_DB_CONFIG, 85 | DIALOG_TITLE); 86 | return; 87 | } 88 | 89 | DbConfig dbConfig = dbConfigOpt.get(); 90 | 91 | try { 92 | if (!testConnection(dbConfig)) { 93 | Messages.showErrorDialog(project, 94 | ERROR_CONNECTION_FAIL, 95 | DIALOG_TITLE); 96 | return; 97 | } 98 | } catch (Exception ex) { 99 | Messages.showErrorDialog(project, 100 | ERROR_CONNECTION_EXCEPTION_PREFIX + ex.getMessage(), 101 | DIALOG_TITLE); 102 | return; 103 | } 104 | 105 | 106 | 107 | 108 | 109 | try { 110 | // 仅支持 SELECT 语句 111 | if (!sql.trim().toLowerCase().startsWith("select")) { 112 | Messages.showWarningDialog(project, 113 | WARN_NOT_SELECT, 114 | DIALOG_TITLE); 115 | return; 116 | } 117 | 118 | List> explainRows = JdbcTableInfoUtil.explainSql(dbConfig, sql); 119 | 120 | if (explainRows == null || explainRows.isEmpty()) { 121 | Messages.showInfoMessage(project, 122 | INFO_NO_RESULT, 123 | DIALOG_TITLE); 124 | return; 125 | } 126 | saveToCache(project, dbConfig); 127 | //缓存sql文件 128 | writeSqlToIdea(project, sql); 129 | 130 | String html = buildExplainHtmlWithChinese(sql, explainRows); 131 | 132 | HtmlViewerUtil.showHtml(project,html, DIALOG_TITLE,runSoarFixed(project),false); 133 | 134 | } catch (Exception ex) { 135 | Messages.showErrorDialog(project, 136 | ERROR_ANALYZE_FAIL_PREFIX + ex.getMessage(), 137 | DIALOG_TITLE); 138 | } 139 | } 140 | 141 | 142 | /** 143 | * 生成执行计划 HTML 144 | */ 145 | private String buildExplainHtmlWithChinese(String sql, List> rows) { 146 | StringBuilder html = new StringBuilder(); 147 | html.append(""); 155 | 156 | html.append("SQL 执行计划分析:
"); 157 | html.append("
").append(sql).append("
"); 158 | 159 | html.append(""); 160 | // 中文列头映射 161 | Map columnTitleMap = new LinkedHashMap<>(); 162 | columnTitleMap.put("id", "序号"); 163 | columnTitleMap.put("select_type", "查询类型"); 164 | columnTitleMap.put("table", "表名"); 165 | columnTitleMap.put("type", "连接类型"); 166 | columnTitleMap.put("possible_keys", "可能使用的索引"); 167 | columnTitleMap.put("key", "实际使用的索引"); 168 | columnTitleMap.put("key_len", "索引长度"); 169 | columnTitleMap.put("ref", "关联字段"); 170 | columnTitleMap.put("rows", "扫描行数"); 171 | columnTitleMap.put("filtered", "过滤比例"); 172 | columnTitleMap.put("Extra", "额外信息"); 173 | 174 | // 获取第一个元素的 keySet(使用 List> 类型 rows) 175 | Set keys = rows.get(0).keySet(); 176 | 177 | for (String col : keys) { 178 | html.append(""); 179 | } 180 | html.append(""); 181 | 182 | for (Map row : rows) { 183 | html.append(""); 184 | for (String key : keys) { 185 | Object value = row.get(key); 186 | String valStr = value != null ? value.toString() : ""; 187 | if ("type".equalsIgnoreCase(key) && "ALL".equalsIgnoreCase(valStr)) { 188 | valStr = "ALL(全表扫描)"; 189 | } 190 | html.append(""); 191 | } 192 | html.append(""); 193 | } 194 | 195 | html.append("
").append(columnTitleMap.getOrDefault(col, col)).append("
").append(valStr).append("
"); 196 | 197 | html.append("
") 198 | .append("字段解释:
") 199 | .append("➤ 序号(id):执行顺序,数字越大优先执行。
") 200 | .append("➤ 查询类型(select_type):表示当前 SELECT 的类型,如 SIMPLE(简单查询)、PRIMARY(主查询)、UNION 等。
") 201 | .append("➤ 表名(table):当前正在访问的表名或临时表名。
") 202 | .append("➤ 连接类型(type):MySQL 选择的数据读取方式,对性能影响很大,按效率从高到低排列如下:
") 203 | .append("    - system:系统表(只有一行),极少见,效率最高。
") 204 | .append("    - const:常量查找,例如主键查询,最多返回一行。
") 205 | .append("    - eq_ref:主键或唯一索引等值连接,性能很高。
") 206 | .append("    - ref:普通索引等值连接,效率略低于 eq_ref。
") 207 | .append("    - range:范围查询,例如使用了 BETWEEN、<、>、IN 等。
") 208 | .append("    - index:全索引扫描,不读取数据表,仅扫描索引。
") 209 | .append("    - ALL:全表扫描,性能最差,应尽量避免。
") 210 | .append("➤ 可能使用的索引(possible_keys):查询可能会用到的索引。
") 211 | .append("➤ 使用索引(key):优化器实际使用的索引。
") 212 | .append("➤ 索引长度(key_len):使用索引的长度,越短越优。
") 213 | .append("➤ 关联字段(ref):显示索引的哪一列被用于查找数据。
") 214 | .append("➤ 扫描行数(rows):MySQL 预估要扫描的行数,值越小越优。
") 215 | .append("➤ 过滤比例(filtered):剩余记录占比,表示该表经过 WHERE 条件过滤后剩余的记录比例。
") 216 | .append("➤ 额外信息(Extra):包含执行过程中的额外信息,如是否使用临时表、文件排序、索引下推等。
") 217 | .append("
"); 218 | 219 | 220 | return html.toString(); 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/main/java/com/chen/entity/SoarConfig.java: -------------------------------------------------------------------------------- 1 | package com.chen.entity; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author czh 7 | * @version 1.0 8 | * @description: 9 | * @date 2025/6/20 8:16 10 | */ 11 | public class SoarConfig { 12 | public Map online_dsn; 13 | public Map test_dsn; 14 | public boolean allow_online_as_test; 15 | public boolean drop_test_temporary; 16 | public boolean only_syntax_check; 17 | public int sampling_statistic_target; 18 | public boolean sampling; 19 | public int log_level; 20 | public String log_output; 21 | public String report_type; 22 | public String[] ignore_rules; 23 | public String blacklist; 24 | public int max_join_table_count; 25 | public int max_group_by_cols_count; 26 | public int max_distinct_count; 27 | public int max_index_cols_count; 28 | public int max_total_rows; 29 | public int spaghetti_query_length; 30 | public boolean allow_drop_index; 31 | public String explain_sql_report_type; 32 | public String explain_type; 33 | public String explain_format; 34 | public String[] explain_warn_select_type; 35 | public String[] explain_warn_access_type; 36 | public int explain_max_keys; 37 | public int explain_min_keys; 38 | public int explain_max_rows; 39 | public String[] explain_warn_extra; 40 | public int explain_max_filtered; 41 | public String[] explain_warn_scalability; 42 | public String query; 43 | public boolean list_heuristic_rules; 44 | public boolean list_test_sqls; 45 | public boolean verbose; 46 | 47 | public static class Dsn { 48 | public String addr; 49 | public String schema; 50 | public String user; 51 | public String password; 52 | public boolean disable; 53 | } 54 | 55 | public Map getOnline_dsn() { 56 | return online_dsn; 57 | } 58 | 59 | public void setOnline_dsn(Map online_dsn) { 60 | this.online_dsn = online_dsn; 61 | } 62 | 63 | public Map getTest_dsn() { 64 | return test_dsn; 65 | } 66 | 67 | public void setTest_dsn(Map test_dsn) { 68 | this.test_dsn = test_dsn; 69 | } 70 | 71 | public boolean isAllow_online_as_test() { 72 | return allow_online_as_test; 73 | } 74 | 75 | public void setAllow_online_as_test(boolean allow_online_as_test) { 76 | this.allow_online_as_test = allow_online_as_test; 77 | } 78 | 79 | public boolean isDrop_test_temporary() { 80 | return drop_test_temporary; 81 | } 82 | 83 | public void setDrop_test_temporary(boolean drop_test_temporary) { 84 | this.drop_test_temporary = drop_test_temporary; 85 | } 86 | 87 | public boolean isOnly_syntax_check() { 88 | return only_syntax_check; 89 | } 90 | 91 | public void setOnly_syntax_check(boolean only_syntax_check) { 92 | this.only_syntax_check = only_syntax_check; 93 | } 94 | 95 | public int getSampling_statistic_target() { 96 | return sampling_statistic_target; 97 | } 98 | 99 | public void setSampling_statistic_target(int sampling_statistic_target) { 100 | this.sampling_statistic_target = sampling_statistic_target; 101 | } 102 | 103 | public boolean isSampling() { 104 | return sampling; 105 | } 106 | 107 | public void setSampling(boolean sampling) { 108 | this.sampling = sampling; 109 | } 110 | 111 | public int getLog_level() { 112 | return log_level; 113 | } 114 | 115 | public void setLog_level(int log_level) { 116 | this.log_level = log_level; 117 | } 118 | 119 | public String getLog_output() { 120 | return log_output; 121 | } 122 | 123 | public void setLog_output(String log_output) { 124 | this.log_output = log_output; 125 | } 126 | 127 | public String getReport_type() { 128 | return report_type; 129 | } 130 | 131 | public void setReport_type(String report_type) { 132 | this.report_type = report_type; 133 | } 134 | 135 | public String[] getIgnore_rules() { 136 | return ignore_rules; 137 | } 138 | 139 | public void setIgnore_rules(String[] ignore_rules) { 140 | this.ignore_rules = ignore_rules; 141 | } 142 | 143 | public String getBlacklist() { 144 | return blacklist; 145 | } 146 | 147 | public void setBlacklist(String blacklist) { 148 | this.blacklist = blacklist; 149 | } 150 | 151 | public int getMax_join_table_count() { 152 | return max_join_table_count; 153 | } 154 | 155 | public void setMax_join_table_count(int max_join_table_count) { 156 | this.max_join_table_count = max_join_table_count; 157 | } 158 | 159 | public int getMax_group_by_cols_count() { 160 | return max_group_by_cols_count; 161 | } 162 | 163 | public void setMax_group_by_cols_count(int max_group_by_cols_count) { 164 | this.max_group_by_cols_count = max_group_by_cols_count; 165 | } 166 | 167 | public int getMax_distinct_count() { 168 | return max_distinct_count; 169 | } 170 | 171 | public void setMax_distinct_count(int max_distinct_count) { 172 | this.max_distinct_count = max_distinct_count; 173 | } 174 | 175 | public int getMax_index_cols_count() { 176 | return max_index_cols_count; 177 | } 178 | 179 | public void setMax_index_cols_count(int max_index_cols_count) { 180 | this.max_index_cols_count = max_index_cols_count; 181 | } 182 | 183 | public int getMax_total_rows() { 184 | return max_total_rows; 185 | } 186 | 187 | public void setMax_total_rows(int max_total_rows) { 188 | this.max_total_rows = max_total_rows; 189 | } 190 | 191 | public int getSpaghetti_query_length() { 192 | return spaghetti_query_length; 193 | } 194 | 195 | public void setSpaghetti_query_length(int spaghetti_query_length) { 196 | this.spaghetti_query_length = spaghetti_query_length; 197 | } 198 | 199 | public boolean isAllow_drop_index() { 200 | return allow_drop_index; 201 | } 202 | 203 | public void setAllow_drop_index(boolean allow_drop_index) { 204 | this.allow_drop_index = allow_drop_index; 205 | } 206 | 207 | public String getExplain_sql_report_type() { 208 | return explain_sql_report_type; 209 | } 210 | 211 | public void setExplain_sql_report_type(String explain_sql_report_type) { 212 | this.explain_sql_report_type = explain_sql_report_type; 213 | } 214 | 215 | public String getExplain_type() { 216 | return explain_type; 217 | } 218 | 219 | public void setExplain_type(String explain_type) { 220 | this.explain_type = explain_type; 221 | } 222 | 223 | public String getExplain_format() { 224 | return explain_format; 225 | } 226 | 227 | public void setExplain_format(String explain_format) { 228 | this.explain_format = explain_format; 229 | } 230 | 231 | public String[] getExplain_warn_select_type() { 232 | return explain_warn_select_type; 233 | } 234 | 235 | public void setExplain_warn_select_type(String[] explain_warn_select_type) { 236 | this.explain_warn_select_type = explain_warn_select_type; 237 | } 238 | 239 | public String[] getExplain_warn_access_type() { 240 | return explain_warn_access_type; 241 | } 242 | 243 | public void setExplain_warn_access_type(String[] explain_warn_access_type) { 244 | this.explain_warn_access_type = explain_warn_access_type; 245 | } 246 | 247 | public int getExplain_max_keys() { 248 | return explain_max_keys; 249 | } 250 | 251 | public void setExplain_max_keys(int explain_max_keys) { 252 | this.explain_max_keys = explain_max_keys; 253 | } 254 | 255 | public int getExplain_min_keys() { 256 | return explain_min_keys; 257 | } 258 | 259 | public void setExplain_min_keys(int explain_min_keys) { 260 | this.explain_min_keys = explain_min_keys; 261 | } 262 | 263 | public int getExplain_max_rows() { 264 | return explain_max_rows; 265 | } 266 | 267 | public void setExplain_max_rows(int explain_max_rows) { 268 | this.explain_max_rows = explain_max_rows; 269 | } 270 | 271 | public String[] getExplain_warn_extra() { 272 | return explain_warn_extra; 273 | } 274 | 275 | public void setExplain_warn_extra(String[] explain_warn_extra) { 276 | this.explain_warn_extra = explain_warn_extra; 277 | } 278 | 279 | public int getExplain_max_filtered() { 280 | return explain_max_filtered; 281 | } 282 | 283 | public void setExplain_max_filtered(int explain_max_filtered) { 284 | this.explain_max_filtered = explain_max_filtered; 285 | } 286 | 287 | public String[] getExplain_warn_scalability() { 288 | return explain_warn_scalability; 289 | } 290 | 291 | public void setExplain_warn_scalability(String[] explain_warn_scalability) { 292 | this.explain_warn_scalability = explain_warn_scalability; 293 | } 294 | 295 | public String getQuery() { 296 | return query; 297 | } 298 | 299 | public void setQuery(String query) { 300 | this.query = query; 301 | } 302 | 303 | public boolean isList_heuristic_rules() { 304 | return list_heuristic_rules; 305 | } 306 | 307 | public void setList_heuristic_rules(boolean list_heuristic_rules) { 308 | this.list_heuristic_rules = list_heuristic_rules; 309 | } 310 | 311 | public boolean isList_test_sqls() { 312 | return list_test_sqls; 313 | } 314 | 315 | public void setList_test_sqls(boolean list_test_sqls) { 316 | this.list_test_sqls = list_test_sqls; 317 | } 318 | 319 | public boolean isVerbose() { 320 | return verbose; 321 | } 322 | 323 | public void setVerbose(boolean verbose) { 324 | this.verbose = verbose; 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/SqlParamUtils.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import java.util.*; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * @author czh 9 | * @version 1.6 10 | * @description: SQL 参数工具类。包含参数提取、参数替换、if判断、foreach展开、set块修正、输入为空则跳过渲染(即便只判断 != null) 11 | * @date 2025/6/25 12 | */ 13 | public class SqlParamUtils { 14 | 15 | public static final String PARAM_REGEX = "[#\\$]\\{(\\w+(?:\\.\\w+)*)}"; 16 | public static final String IF_BLOCK_REGEX = "([\\s\\S]*?)"; 17 | public static final String FOREACH_REGEX = "]*collection=\"(\\w+)\"[^>]*>([\\s\\S]*?)"; 18 | public static final String SQL_WHITESPACE_REGEX = "[\\t ]+"; 19 | public static final String SQL_MULTILINE_REGEX = "(\\r?\\n){2,}"; 20 | public static final String SET_BLOCK_REGEX = "([\\s\\S]*?)"; 21 | 22 | /** 23 | * 提取 SQL 中的参数名 24 | * 25 | * @param sql SQL字符串 26 | * @return 参数名集合,按出现顺序 27 | */ 28 | public static Set extractParams(String sql) { 29 | Set params = new LinkedHashSet<>(); 30 | Matcher matcher = Pattern.compile(PARAM_REGEX).matcher(sql); 31 | while (matcher.find()) params.add(matcher.group(1)); 32 | return params; 33 | } 34 | 35 | /** 36 | * 根据参数值渲染最终 SQL,支持 if/foreach/set 模板语法 37 | * 38 | * @param sql SQL模板 39 | * @param paramValues 参数值映射 40 | * @return 渲染后的 SQL 41 | */ 42 | public static String buildFinalSql(String sql, Map paramValues) { 43 | sql = processIfBlocks(sql, paramValues); 44 | sql = processForeachBlocks(sql, paramValues); 45 | sql = processSetBlocks(sql); 46 | sql= processWhereTag(sql); 47 | 48 | for (Map.Entry entry : paramValues.entrySet()) { 49 | String key = entry.getKey(); 50 | Object valObj = entry.getValue(); 51 | if (valObj == null || valObj.toString().trim().isEmpty()) { 52 | sql = sql.replaceAll("[#\\$]\\{" + Pattern.quote(key) + "}", ""); 53 | continue; 54 | } 55 | String value = String.valueOf(valObj); 56 | sql = sql.replaceAll("#\\{" + Pattern.quote(key) + "}", isNumber(value) ? value : ("'" + value + "'")); 57 | sql = sql.replaceAll("\\$\\{" + Pattern.quote(key) + "}", value); 58 | } 59 | return sql.replaceAll("", "") 60 | .replaceAll(SQL_WHITESPACE_REGEX, " ") 61 | .replaceAll(SQL_MULTILINE_REGEX, "\n") 62 | .trim(); 63 | } 64 | 65 | /** 66 | * 判断字符串是否为数字 67 | * 68 | * @param s 输入字符串 69 | * @return 是否数字 70 | */ 71 | public static boolean isNumber(String s) { 72 | return s != null && s.matches("^-?\\d+(\\.\\d+)?$"); 73 | } 74 | 75 | 76 | /** 77 | * 处理 SQL 里的 块 78 | * 79 | * @param sql SQL模板 80 | * @param paramValues 参数值映射 81 | * @return 处理后的 SQL 82 | */ 83 | public static String processIfBlocks(String sql, Map paramValues) { 84 | Pattern ifBlock = Pattern.compile(IF_BLOCK_REGEX, Pattern.DOTALL); 85 | Matcher matcher = ifBlock.matcher(sql); 86 | StringBuffer sb = new StringBuffer(); 87 | while (matcher.find()) { 88 | String condition = matcher.group(1); 89 | String blockSql = matcher.group(2); 90 | boolean show = evalCondition(condition, paramValues); 91 | matcher.appendReplacement(sb, show ? Matcher.quoteReplacement(blockSql) : ""); 92 | } 93 | matcher.appendTail(sb); 94 | return sb.toString(); 95 | } 96 | 97 | /** 98 | * 模拟 MyBatis 的 标签逻辑: 99 | * - 去除无用的 标签 100 | * - 自动添加 WHERE 101 | * - 移除第一个 AND 或 OR 102 | */ 103 | public static String processWhereTag(String sql) { 104 | Pattern pattern = Pattern.compile("(.*?)", Pattern.DOTALL); 105 | Matcher matcher = pattern.matcher(sql); 106 | StringBuffer sb = new StringBuffer(); 107 | 108 | while (matcher.find()) { 109 | String body = matcher.group(1).trim(); 110 | 111 | // 去除最前面的 and / or 112 | body = body.replaceFirst("(?i)^\\s*(and|or)\\s+", ""); 113 | 114 | if (!body.isEmpty()) { 115 | matcher.appendReplacement(sb, "WHERE " + Matcher.quoteReplacement(body)); 116 | } else { 117 | matcher.appendReplacement(sb, ""); 118 | } 119 | } 120 | 121 | matcher.appendTail(sb); 122 | return sb.toString(); 123 | } 124 | 125 | /** 126 | * 判断 if test 内的条件是否成立 127 | * 只支持 xx != null, xx != '', xx != 0 的多 and 条件 128 | * 129 | * @param condition 条件表达式 130 | * @param paramValues 参数值映射 131 | * @return 是否成立 132 | */ 133 | public static boolean evalCondition(String condition, Map paramValues) { 134 | String[] ands = condition.split("and"); 135 | for (String cond : ands) { 136 | cond = cond.trim(); 137 | Matcher keyMatch = Pattern.compile("(\\w+)").matcher(cond); 138 | if (keyMatch.find()) { 139 | String key = keyMatch.group(1); 140 | Object val = paramValues.get(key); 141 | if (cond.contains("!= null")) { 142 | if (val == null || val.toString().trim().isEmpty()) return false; 143 | } else if (cond.contains("!= ''")) { 144 | if (val == null || val.toString().trim().isEmpty()) return false; 145 | } else if (cond.contains("!= 0")) { 146 | if (val == null || "0".equals(val.toString().trim())) return false; 147 | } 148 | } 149 | } 150 | return true; 151 | } 152 | 153 | /** 154 | * 处理带的SQL模板,支持弹窗输入userId为单个或逗号分隔的字符串 155 | * @param sql SQL模板 156 | * @param paramValues 参数map(支持userIds传"1,2,3"或List) 157 | * @return 替换后的SQL 158 | */ 159 | public static String processForeachBlocks(String sql, Map paramValues) { 160 | Pattern foreachPattern = Pattern.compile( 161 | "]*)>([\\s\\S]*?)", 162 | Pattern.CASE_INSENSITIVE | Pattern.DOTALL); 163 | Matcher matcher = foreachPattern.matcher(sql); 164 | StringBuffer sb = new StringBuffer(); 165 | 166 | while (matcher.find()) { 167 | String attrStr = matcher.group(1); 168 | String content = matcher.group(2); 169 | 170 | // 只关心item,不判定collection 171 | String itemAlias = getAttr(attrStr, "item"); 172 | String open = getAttr(attrStr, "open", "("); 173 | String separator = getAttr(attrStr, "separator", ","); 174 | String close = getAttr(attrStr, "close", ")"); 175 | 176 | // 只从paramValues取itemAlias 177 | Object valueObj = paramValues.get(itemAlias); 178 | List list = parseList(valueObj); 179 | 180 | StringBuilder fragment = new StringBuilder(); 181 | fragment.append(open); 182 | for (int i = 0; i < list.size(); i++) { 183 | String val = list.get(i); 184 | String itemSql = content; 185 | itemSql = itemSql.replace( 186 | "#{" + itemAlias + "}", 187 | isNumber(val) ? val : ("'" + val + "'") 188 | ); 189 | itemSql = itemSql.replace( 190 | "${" + itemAlias + "}", 191 | val 192 | ); 193 | fragment.append(itemSql.trim()); 194 | if (i < list.size() - 1) fragment.append(separator); 195 | } 196 | fragment.append(close); 197 | 198 | matcher.appendReplacement(sb, Matcher.quoteReplacement(fragment.toString())); 199 | } 200 | matcher.appendTail(sb); 201 | return sb.toString(); 202 | } 203 | 204 | private static List parseList(Object obj) { 205 | if (obj == null) return Collections.emptyList(); 206 | if (obj instanceof List) { 207 | List origin = (List) obj; 208 | List ret = new ArrayList<>(); 209 | for (Object o : origin) ret.add(o == null ? "" : o.toString().trim()); 210 | return ret; 211 | } 212 | if (obj instanceof String) { 213 | String s = ((String)obj).trim(); 214 | if (s.isEmpty()) return Collections.emptyList(); 215 | if (s.contains(",")) { 216 | List ret = new ArrayList<>(); 217 | for (String part : s.split(",")) ret.add(part.trim()); 218 | return ret; 219 | } else { 220 | return Collections.singletonList(s); 221 | } 222 | } 223 | return Collections.singletonList(obj.toString().trim()); 224 | } 225 | 226 | private static String getAttr(String attrStr, String attrName) { 227 | return getAttr(attrStr, attrName, ""); 228 | } 229 | private static String getAttr(String attrStr, String attrName, String defaultVal) { 230 | Pattern p = Pattern.compile(attrName + "=\"([^\"]*)\""); 231 | Matcher m = p.matcher(attrStr); 232 | return m.find() ? m.group(1) : defaultVal; 233 | } 234 | 235 | 236 | 237 | /** 238 | * 处理 块,自动补全逗号,去除末尾多余逗号 239 | * 240 | * @param sql SQL模板 241 | * @return 处理后的 SQL 242 | */ 243 | public static String processSetBlocks(String sql) { 244 | Pattern setBlock = Pattern.compile(SET_BLOCK_REGEX, Pattern.DOTALL); 245 | Matcher matcher = setBlock.matcher(sql); 246 | StringBuffer sb = new StringBuffer(); 247 | while (matcher.find()) { 248 | String content = matcher.group(1).trim(); 249 | String[] lines = content.split("\r?\n"); 250 | StringBuilder cleaned = new StringBuilder(); 251 | for (String line : lines) { 252 | String trimmed = line.trim(); 253 | if (trimmed.isEmpty()) continue; 254 | cleaned.append(trimmed); 255 | if (!trimmed.endsWith(",")) cleaned.append(","); 256 | cleaned.append("\n"); 257 | } 258 | if (cleaned.length() > 0 && cleaned.charAt(cleaned.length() - 2) == ',') { 259 | cleaned.deleteCharAt(cleaned.length() - 2); 260 | } 261 | matcher.appendReplacement(sb, "SET\n" + Matcher.quoteReplacement(cleaned.toString().trim())); 262 | } 263 | matcher.appendTail(sb); 264 | return sb.toString(); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/SoarYamlUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.entity.DbConfig; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.ui.Messages; 7 | 8 | import java.io.*; 9 | import java.net.URL; 10 | import java.nio.charset.StandardCharsets; 11 | import java.nio.file.Files; 12 | import java.nio.file.Path; 13 | import java.nio.file.Paths; 14 | import java.nio.file.StandardCopyOption; 15 | import java.text.SimpleDateFormat; 16 | import java.time.format.DateTimeFormatter; 17 | import java.util.*; 18 | 19 | import static com.chen.constant.FileConstant.*; 20 | 21 | /** 22 | * @author czh 23 | * @version 1.0 24 | * @description: 25 | * @date 2025/6/20 8:23 26 | */ 27 | public class SoarYamlUtil { 28 | 29 | public static String readYamlTemplate(String resourcePath) throws IOException { 30 | try (InputStream is = SoarYamlUtil.class.getClassLoader().getResourceAsStream(resourcePath)) { 31 | if (is == null) throw new IOException("资源文件未找到: " + resourcePath); 32 | 33 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { 34 | StringBuilder sb = new StringBuilder(); 35 | String line; 36 | while ((line = reader.readLine()) != null) { 37 | sb.append(line).append("\n"); 38 | } 39 | return sb.toString(); 40 | } 41 | } 42 | } 43 | 44 | public static String readResourceFile(String path) throws IOException { 45 | try (InputStream is = SoarYamlUtil.class.getClassLoader().getResourceAsStream(path)) { 46 | if (is == null) throw new IOException("资源文件未找到: " + path); 47 | return new String(is.readAllBytes(), StandardCharsets.UTF_8); 48 | } 49 | } 50 | 51 | /** 52 | * 复制resources目录下的soar.exe到目标目录 53 | * 54 | * @param targetDir 目标目录路径,例如 .idea目录的绝对路径 55 | * @throws IOException 56 | */ 57 | public static void copySoarExeToDir(Path targetDir) throws IOException { 58 | if (!Files.exists(targetDir)) { 59 | Files.createDirectories(targetDir); 60 | } 61 | Path targetFile = targetDir.resolve("soar.exe"); 62 | if (Files.exists(targetFile)) { 63 | return; 64 | } 65 | // 资源路径(resources目录下的soar.exe) 66 | try (InputStream in = SoarYamlUtil.class.getClassLoader().getResourceAsStream("soar.exe")) { 67 | if (in == null) { 68 | throw new FileNotFoundException("资源目录下找不到 soar.exe"); 69 | } 70 | 71 | Files.copy(in, targetFile, StandardCopyOption.REPLACE_EXISTING); 72 | } 73 | } 74 | 75 | /** 76 | * 根据 json 配置替换 yaml 内容中的数据库连接信息 77 | * 78 | * @param yamlTemplate yaml 模板内容字符串 79 | */ 80 | public static String replaceDbConfigInYaml(DbConfig config, String yamlTemplate) { 81 | String url = config.getUrl(); 82 | String username = config.getUsername(); // 或者 getUser() 83 | String password = config.getPassword(); 84 | 85 | String addr = url.substring(url.indexOf("//") + 2, url.indexOf("/", url.indexOf("//") + 2)); 86 | String schema = url.substring(url.indexOf("/", url.indexOf("//") + 2) + 1, 87 | url.contains("?") ? url.indexOf("?") : url.length()); 88 | 89 | String replaced = yamlTemplate; 90 | replaced = replaced.replaceAll("(?m)^([ \\t]*)addr:\\s*.*", "$1addr: " + addr); 91 | replaced = replaced.replaceAll("(?m)^([ \\t]*)schema:\\s*.*", "$1schema: " + schema); 92 | replaced = replaced.replaceAll("(?m)^([ \\t]*)user:\\s*.*", "$1user: " + username); 93 | replaced = replaced.replaceAll("(?m)^([ \\t]*)password:\\s*.*", "$1password: " + password); 94 | 95 | return replaced; 96 | } 97 | 98 | 99 | /** 100 | * 写内容到用户项目的.idea目录下的soar.yaml 101 | * 102 | * @param projectBasePath 用户项目根路径 103 | * @param content 替换好的 yaml 内容 104 | * @throws IOException 105 | */ 106 | public static void writeYamlToProjectIdea(String projectBasePath, String content) throws IOException { 107 | Path yamlPath = Paths.get(projectBasePath, SOARYMAL_PATH); 108 | Files.createDirectories(yamlPath.getParent()); 109 | Files.writeString(yamlPath, content, StandardCharsets.UTF_8); 110 | } 111 | 112 | /** 113 | * 把 sqlContent 写入到用户项目 .idea/idea.sql 文件 114 | * 115 | * @param sqlContent SQL 脚本内容 116 | * @throws IOException 117 | */ 118 | public static void writeSqlToIdea(Project project, String sqlContent) throws IOException { 119 | String projectBasePath = project.getBasePath(); 120 | Path sqlFilePath = Paths.get(projectBasePath, SQL_SCRIPT_FILE_NAME); 121 | Files.createDirectories(sqlFilePath.getParent()); // 确保.idea目录存在 122 | Files.writeString(sqlFilePath, sqlContent, StandardCharsets.UTF_8); 123 | } 124 | 125 | /** 126 | * 获取当前时间戳(用于生成报告文件名) 127 | */ 128 | private static String getTimestamp() { 129 | return new SimpleDateFormat(DATE_FORMAT).format(new Date()); 130 | } 131 | 132 | /** 133 | * 获取 .idea 目录下某个文件的完整路径 134 | */ 135 | private static Path getIdeaPath(Project project, String fileName) { 136 | return Paths.get(project.getBasePath(), IDEA_DIR, fileName); 137 | } 138 | 139 | /** 140 | * 确保结果输出目录存在,若不存在则创建 141 | */ 142 | private static File ensureResultDir(String file) throws IOException { 143 | File resultDir = new File(RESULT_DIR+file); 144 | if (!resultDir.exists() && !resultDir.mkdirs()) { 145 | throw new IOException("无法创建结果目录 " + RESULT_DIR); 146 | } 147 | return resultDir; 148 | } 149 | 150 | /** 151 | * 运行分析命令并返回控制台输出(不生成文件) 152 | */ 153 | public static String runSoarFixed(Project project) throws Exception { 154 | Path soarExePath = getIdeaPath(project, SOAR_EXE_NAME); 155 | Path sqlFilePath = getIdeaPath(project, SQL_FILE_NAME); 156 | 157 | List command = List.of( 158 | soarExePath.toString(), 159 | "-query=" + sqlFilePath 160 | ); 161 | 162 | ProcessBuilder builder = new ProcessBuilder(command); 163 | builder.redirectErrorStream(true); 164 | 165 | StringBuilder output = new StringBuilder(); 166 | Process process = null; 167 | 168 | try { 169 | DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 170 | output.append("执行时间: ").append(dtf.format(java.time.LocalDateTime.now())).append(System.lineSeparator()); 171 | process = builder.start(); 172 | try (BufferedReader reader = new BufferedReader( 173 | new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { 174 | String line; 175 | while ((line = reader.readLine()) != null) { 176 | if (line.contains("beelogger") || line.contains("jsonconfig must have filename") || line.contains("a:link")) { 177 | continue; 178 | } 179 | output.append(line).append(System.lineSeparator()); 180 | } 181 | } 182 | int exitCode = process.waitFor(); 183 | if (exitCode != 0) { 184 | output.append("进程异常退出,退出码: ").append(exitCode).append(System.lineSeparator()); 185 | } 186 | } catch (Exception e) { 187 | e.printStackTrace(); 188 | output.append("执行异常: ").append(e.getMessage()).append(System.lineSeparator()); 189 | } finally { 190 | if (process != null) process.destroy(); 191 | } 192 | return output.toString(); 193 | } 194 | 195 | /** 196 | * 执行分析结果保存为 HTML 文件 197 | */ 198 | public static void downHtml(Project project) throws Exception { 199 | File soarExe = getIdeaPath(project, SOAR_EXE_NAME).toFile(); 200 | File sqlFile = getIdeaPath(project, SQL_FILE_NAME).toFile(); 201 | 202 | if (!soarExe.exists() || !sqlFile.exists()) { 203 | throw new FileNotFoundException("soar.exe 或 SQL 文件不存在!"); 204 | } 205 | 206 | File resultDir = ensureResultDir("\\html"); 207 | File outputFile = new File(resultDir, REPORT_PREFIX + getTimestamp() + HTML_SUFFIX); 208 | 209 | ProcessBuilder pb = new ProcessBuilder( 210 | soarExe.getAbsolutePath(), 211 | "-query=" + sqlFile.getAbsolutePath(), 212 | "-log-output=stdout" 213 | ); 214 | pb.redirectErrorStream(true); 215 | 216 | Process process = pb.start(); 217 | 218 | try (BufferedReader reader = new BufferedReader( 219 | new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); 220 | BufferedWriter writer = new BufferedWriter( 221 | new OutputStreamWriter(new FileOutputStream(outputFile), StandardCharsets.UTF_8))) { 222 | 223 | String line; 224 | while ((line = reader.readLine()) != null) { 225 | writer.write(line); 226 | writer.newLine(); 227 | } 228 | } 229 | 230 | if (process.waitFor() != 0) { 231 | throw new RuntimeException("执行失败,退出码:" + process.exitValue()); 232 | } 233 | 234 | Messages.showInfoMessage(project, "HTML 报告生成成功:\n" + outputFile.getAbsolutePath(), "生成成功"); 235 | } 236 | 237 | /** 238 | * 执行分析结果保存为 Markdown 文件 239 | * 会修改 soar.yaml 报告类型配置,完成后再还原 240 | */ 241 | public static void downMD(Project project) throws Exception { 242 | Path yamlPath = getIdeaPath(project, YAML_FILE_NAME); 243 | Path soarExePath = getIdeaPath(project, SOAR_EXE_NAME); 244 | Path sqlFilePath = getIdeaPath(project, SQL_FILE_NAME); 245 | 246 | if (!Files.exists(yamlPath) || !Files.exists(soarExePath) || !Files.exists(sqlFilePath)) { 247 | throw new FileNotFoundException("配置文件、soar.exe 或 SQL 文件不存在!"); 248 | } 249 | 250 | String originalYaml = Files.readString(yamlPath, StandardCharsets.UTF_8); 251 | String modifiedYaml = originalYaml.replace("report-type: html", "report-type: markdown"); 252 | 253 | Files.writeString(yamlPath, modifiedYaml, StandardCharsets.UTF_8); 254 | 255 | File outputMd = new File(ensureResultDir("\\md"), REPORT_PREFIX + getTimestamp() + MD_SUFFIX); 256 | 257 | ProcessBuilder pb = new ProcessBuilder( 258 | soarExePath.toAbsolutePath().toString(), 259 | "-query=" + sqlFilePath.toAbsolutePath(), 260 | "-log-output=stdout" 261 | ); 262 | pb.redirectErrorStream(true); 263 | 264 | Process process = pb.start(); 265 | 266 | try (BufferedReader reader = new BufferedReader( 267 | new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8)); 268 | BufferedWriter writer = new BufferedWriter( 269 | new OutputStreamWriter(new FileOutputStream(outputMd), StandardCharsets.UTF_8))) { 270 | 271 | String line; 272 | while ((line = reader.readLine()) != null) { 273 | writer.write(line); 274 | writer.newLine(); 275 | } 276 | } 277 | 278 | if (process.waitFor() != 0) { 279 | throw new RuntimeException("执行失败,退出码:" + process.exitValue()); 280 | } 281 | 282 | // 恢复原 YAML 283 | Files.writeString(yamlPath, originalYaml, StandardCharsets.UTF_8); 284 | 285 | Messages.showInfoMessage(project, "Markdown 报告生成成功:\n" + outputMd.getAbsolutePath(), "生成成功"); 286 | } 287 | 288 | /** 289 | * 重写 SQL 并直接在弹窗中展示(不写入文件) 290 | */ 291 | public static String rewriteSQL(Project project) throws Exception { 292 | 293 | Path yamlPath = getIdeaPath(project, YAML_FILE_NAME); 294 | Path soarExePath = getIdeaPath(project, SOAR_EXE_NAME); 295 | Path sqlFilePath = getIdeaPath(project, SQL_FILE_NAME); 296 | 297 | if (!Files.exists(yamlPath) || !Files.exists(soarExePath) || !Files.exists(sqlFilePath)) { 298 | throw new FileNotFoundException("配置文件、soar.exe 或 SQL 文件不存在!"); 299 | } 300 | 301 | String originalYaml = Files.readString(yamlPath, StandardCharsets.UTF_8); 302 | String modifiedYaml = originalYaml.replace("report-type: html", "report-type: rewrite"); 303 | Files.writeString(yamlPath, modifiedYaml, StandardCharsets.UTF_8); 304 | List command = List.of( 305 | soarExePath.toString(), 306 | "-query=" + sqlFilePath, 307 | "-log-output=stdout" 308 | ); 309 | 310 | ProcessBuilder builder = new ProcessBuilder(command); 311 | builder.redirectErrorStream(true); 312 | 313 | StringBuilder output = new StringBuilder(); 314 | Process process = null; 315 | 316 | try { 317 | process = builder.start(); 318 | try (BufferedReader reader = new BufferedReader( 319 | new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) { 320 | String line; 321 | while ((line = reader.readLine()) != null) { 322 | output.append(line).append(System.lineSeparator()); 323 | } 324 | } 325 | int exitCode = process.waitFor(); 326 | if (exitCode != 0) { 327 | output.append("进程异常退出,退出码: ").append(exitCode).append(System.lineSeparator()); 328 | } 329 | } catch (Exception e) { 330 | e.printStackTrace(); 331 | output.append("执行异常: ").append(e.getMessage()).append(System.lineSeparator()); 332 | } finally { 333 | if (process != null) process.destroy(); 334 | } 335 | 336 | return output.toString(); 337 | } 338 | 339 | 340 | } 341 | -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/JdbcTableInfoUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | 3 | import com.chen.constant.DataSourceConstants; 4 | import com.chen.entity.ColumnMeta; 5 | import com.chen.entity.DbConfig; 6 | import com.chen.entity.TableMeta; 7 | 8 | import java.sql.*; 9 | import java.util.*; 10 | 11 | import static com.chen.utils.DbConfigUtil.parseDbType; 12 | import static java.sql.DriverManager.getConnection; 13 | 14 | /** 15 | * JDBC 工具类,用于获取指定表的字段元数据信息 16 | * 包括字段名称、类型、是否主键和备注信息 17 | *

18 | * 依赖 MySQL 数据库驱动:com.mysql.cj.jdbc.Driver 19 | * 20 | * @author czh 21 | * @version 1.0 22 | * @date 2025/6/12 14:44 23 | */ 24 | public class JdbcTableInfoUtil { 25 | 26 | private static final Map DRIVER_MAP = Map.of( 27 | DataSourceConstants.DB_TYPE_MYSQL, DataSourceConstants.MYSQL_DRIVER, 28 | DataSourceConstants.DB_TYPE_ORACLE, DataSourceConstants.ORACLE_DRIVER, 29 | DataSourceConstants.DB_TYPE_SQLSERVER, DataSourceConstants.SQLSERVER_DRIVER, 30 | DataSourceConstants.DB_TYPE_POSTGRESQL, DataSourceConstants.POSTGRESQL_DRIVER 31 | // 可继续扩展 32 | ); 33 | 34 | /** 35 | * 获取指定表的字段元数据列表 36 | * 37 | * @param dbConfig 数据库连接配置 38 | * @param tableName 表名 39 | * @return 字段元数据列表(ColumnMeta) 40 | * @throws ClassNotFoundException 如果 JDBC 驱动未加载 41 | */ 42 | public static TableMeta getTableMetaFromMySQL(DbConfig dbConfig, String tableName) { 43 | try (Connection conn = DataSourceManager.getDataSource(dbConfig).getConnection()) { 44 | DatabaseMetaData meta = conn.getMetaData(); 45 | 46 | String catalog = null; 47 | String url = dbConfig.getUrl(); 48 | if (url != null) { 49 | int idx1 = url.indexOf("/", "jdbc:mysql://".length()); 50 | int idx2 = url.indexOf("?", idx1); 51 | if (idx1 != -1) { 52 | catalog = (idx2 != -1) ? url.substring(idx1 + 1, idx2) : url.substring(idx1 + 1); 53 | } 54 | } 55 | 56 | // ✅ 获取表注释(表备注) 57 | String tableComment = tableName; 58 | String commentSql = "SELECT TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?"; 59 | try (PreparedStatement ps = conn.prepareStatement(commentSql)) { 60 | ps.setString(1, catalog); 61 | ps.setString(2, tableName); 62 | try (ResultSet rs = ps.executeQuery()) { 63 | if (rs.next()) { 64 | String comment = rs.getString("TABLE_COMMENT"); 65 | if (comment != null && !comment.isBlank()) { 66 | tableComment = comment; 67 | } 68 | } 69 | } 70 | } 71 | 72 | // ✅ 获取主键字段 73 | Set pkSet = new HashSet<>(); 74 | try (ResultSet pkRs = meta.getPrimaryKeys(catalog, null, tableName)) { 75 | while (pkRs.next()) { 76 | pkSet.add(pkRs.getString("COLUMN_NAME")); 77 | } 78 | } 79 | 80 | // ✅ 获取索引字段 81 | Set indexSet = new HashSet<>(); 82 | try (ResultSet idxRs = meta.getIndexInfo(catalog, null, tableName, false, false)) { 83 | while (idxRs.next()) { 84 | String colName = idxRs.getString("COLUMN_NAME"); 85 | if (colName != null) { 86 | indexSet.add(colName); 87 | } 88 | } 89 | } 90 | 91 | // ✅ 获取字段列表 92 | List columns = new ArrayList<>(); 93 | try (ResultSet rs = meta.getColumns(catalog, null, tableName, null)) { 94 | while (rs.next()) { 95 | String name = rs.getString("COLUMN_NAME"); 96 | String type = rs.getString("TYPE_NAME"); 97 | String remark = rs.getString("REMARKS"); 98 | boolean pk = pkSet.contains(name); 99 | boolean idx = indexSet.contains(name); 100 | columns.add(new ColumnMeta(name, type, pk, idx, remark)); 101 | } 102 | } 103 | 104 | return new TableMeta(tableName, tableComment, columns); 105 | 106 | } catch (SQLException e) { 107 | throw new RuntimeException(e); 108 | } 109 | } 110 | 111 | 112 | 113 | /** 114 | * 获取 SQL Server 表字段元数据 115 | * 116 | * @param dbConfig 数据库配置 117 | * @param tableName 表名 118 | * @return 字段元数据列表 119 | */ 120 | public static TableMeta getTableColumnsFromSqlServer(DbConfig dbConfig, String tableName) { 121 | try (Connection conn = DataSourceManager.getDataSource(dbConfig).getConnection()) { 122 | DatabaseMetaData meta = conn.getMetaData(); 123 | 124 | // 获取当前数据库名(Catalog) 125 | String catalog = conn.getCatalog(); 126 | 127 | // 表注释 128 | String tableComment = tableName; 129 | String commentSql = "SELECT TABLE_NAME, TABLE_SCHEMA, TABLE_CATALOG, TABLE_TYPE, REMARKS " + 130 | "FROM INFORMATION_SCHEMA.TABLES " + 131 | "WHERE TABLE_CATALOG = ? AND TABLE_NAME = ?"; 132 | try (PreparedStatement ps = conn.prepareStatement(commentSql)) { 133 | ps.setString(1, catalog); 134 | ps.setString(2, tableName); 135 | try (ResultSet rs = ps.executeQuery()) { 136 | if (rs.next()) { 137 | String comment = rs.getString("REMARKS"); 138 | if (comment != null && !comment.isBlank()) { 139 | tableComment = comment; 140 | } 141 | } 142 | } 143 | } 144 | 145 | // 主键集合 146 | Set pkSet = new HashSet<>(); 147 | try (ResultSet pkRs = meta.getPrimaryKeys(catalog, null, tableName)) { 148 | while (pkRs.next()) { 149 | pkSet.add(pkRs.getString("COLUMN_NAME")); 150 | } 151 | } 152 | 153 | // 索引字段集合 154 | Set indexSet = new HashSet<>(); 155 | try (ResultSet idxRs = meta.getIndexInfo(catalog, null, tableName, false, false)) { 156 | while (idxRs.next()) { 157 | String colName = idxRs.getString("COLUMN_NAME"); 158 | if (colName != null) { 159 | indexSet.add(colName); 160 | } 161 | } 162 | } 163 | 164 | // 字段信息 165 | List columns = new ArrayList<>(); 166 | try (ResultSet rs = meta.getColumns(catalog, null, tableName, null)) { 167 | while (rs.next()) { 168 | String name = rs.getString("COLUMN_NAME"); 169 | String type = rs.getString("TYPE_NAME"); 170 | String remark = rs.getString("REMARKS"); 171 | boolean pk = pkSet.contains(name); 172 | boolean idx = indexSet.contains(name); 173 | columns.add(new ColumnMeta(name, type, pk, idx, remark)); 174 | } 175 | } 176 | 177 | return new TableMeta(tableName, tableComment, columns); 178 | } catch (SQLException e) { 179 | throw new RuntimeException("获取 SQL Server 字段失败: " + e.getMessage(), e); 180 | } 181 | } 182 | 183 | 184 | 185 | 186 | /** 187 | * 获取 Oracle 表字段元数据 188 | * 189 | * @param dbConfig 数据库配置 190 | * @param tableName 表名 191 | * @return 字段元数据列表 192 | */ 193 | public static TableMeta getTableColumnsFromOracle(DbConfig dbConfig, String tableName) { 194 | List columns = new ArrayList<>(); 195 | Set pkSet = new HashSet<>(); 196 | Set indexSet = new HashSet<>(); 197 | String schema = dbConfig.getUsername().toUpperCase(); 198 | String upperTableName = tableName.toUpperCase(); 199 | String tableComment = tableName; 200 | 201 | try (Connection conn = DataSourceManager.getDataSource(dbConfig).getConnection()) { 202 | DatabaseMetaData meta = conn.getMetaData(); 203 | 204 | // 表注释 205 | String commentSql = "SELECT COMMENTS FROM ALL_TAB_COMMENTS WHERE OWNER = ? AND TABLE_NAME = ?"; 206 | try (PreparedStatement ps = conn.prepareStatement(commentSql)) { 207 | ps.setString(1, schema); 208 | ps.setString(2, upperTableName); 209 | try (ResultSet rs = ps.executeQuery()) { 210 | if (rs.next()) { 211 | String comment = rs.getString("COMMENTS"); 212 | if (comment != null && !comment.isBlank()) { 213 | tableComment = comment; 214 | } 215 | } 216 | } 217 | } 218 | 219 | // 主键字段 220 | try (ResultSet pkRs = meta.getPrimaryKeys(null, schema, upperTableName)) { 221 | while (pkRs.next()) { 222 | pkSet.add(pkRs.getString("COLUMN_NAME")); 223 | } 224 | } 225 | 226 | // 索引字段 227 | try (ResultSet idxRs = meta.getIndexInfo(null, schema, upperTableName, false, false)) { 228 | while (idxRs.next()) { 229 | String colName = idxRs.getString("COLUMN_NAME"); 230 | if (colName != null) { 231 | indexSet.add(colName); 232 | } 233 | } 234 | } 235 | 236 | // 字段信息(带注释) 237 | String columnSql = "SELECT col.COLUMN_NAME, col.DATA_TYPE, com.COMMENTS " + 238 | "FROM ALL_TAB_COLUMNS col " + 239 | "LEFT JOIN ALL_COL_COMMENTS com " + 240 | "ON col.OWNER = com.OWNER AND col.TABLE_NAME = com.TABLE_NAME AND col.COLUMN_NAME = com.COLUMN_NAME " + 241 | "WHERE col.OWNER = ? AND col.TABLE_NAME = ?"; 242 | try (PreparedStatement ps = conn.prepareStatement(columnSql)) { 243 | ps.setString(1, schema); 244 | ps.setString(2, upperTableName); 245 | try (ResultSet rs = ps.executeQuery()) { 246 | while (rs.next()) { 247 | String name = rs.getString("COLUMN_NAME"); 248 | String type = rs.getString("DATA_TYPE"); 249 | String remark = rs.getString("COMMENTS"); 250 | boolean pk = pkSet.contains(name); 251 | boolean idx = indexSet.contains(name); 252 | columns.add(new ColumnMeta(name, type, pk, idx, remark)); 253 | } 254 | } 255 | } 256 | } catch (SQLException e) { 257 | throw new RuntimeException("获取 Oracle 字段失败: " + e.getMessage(), e); 258 | } 259 | 260 | return new TableMeta(tableName, tableComment, columns); 261 | } 262 | 263 | 264 | 265 | /** 266 | * 获取指定表的字段元数据 267 | * 268 | * @param dbConfig 数据库连接配置 269 | * @param tableName 表名 270 | * @return 字段元数据列表 271 | * @throws Exception 如果获取失败 272 | */ 273 | public static TableMeta getTableColumns(DbConfig dbConfig, String tableName) throws Exception { 274 | return ColumnMetaUtils.getTableColumns(dbConfig, tableName); 275 | } 276 | 277 | 278 | 279 | /** 280 | * 检查数据库连接是否有效 281 | * 282 | * @param dbConfig 数据库连接配置 283 | * @return true 表示连接成功,false 表示连接失败 284 | */ 285 | public static boolean testConnection(DbConfig dbConfig) throws Exception { 286 | String dbType = parseDbType(dbConfig.getUrl()); 287 | String driverClass = Optional.ofNullable(DRIVER_MAP.get(dbType)) 288 | .orElseThrow(() -> new RuntimeException("不支持的数据库类型: " + dbType)); 289 | Class.forName(driverClass); 290 | 291 | try (Connection conn = getConnection( 292 | dbConfig.getUrl(), 293 | dbConfig.getUsername(), 294 | dbConfig.getPassword())) { 295 | return conn != null && !conn.isClosed(); 296 | } 297 | } 298 | 299 | /** 300 | * 执行 EXPLAIN 查询,并返回每行结果映射列表 301 | * 只支持 MySQL 的 EXPLAIN 302 | * 303 | * @param config 数据库连接配置 304 | * @param sql 要执行 EXPLAIN 的 SQL 查询(必须是 SELECT) 305 | * @return 结果集每行对应的 Map 集合列表 306 | * @throws Exception 执行异常 307 | */ 308 | public static List> explainSql(DbConfig config, String sql) throws Exception { 309 | if (sql == null || !sql.trim().toLowerCase().startsWith("select")) { 310 | throw new IllegalArgumentException("只支持 SELECT 语句的执行计划"); 311 | } 312 | 313 | String explainSql = "EXPLAIN " + sql; 314 | 315 | try (Connection conn = DataSourceManager.getDataSource(config).getConnection()) { 316 | PreparedStatement ps = conn.prepareStatement(explainSql); 317 | ResultSet rs = ps.executeQuery(); 318 | 319 | ResultSetMetaData metaData = rs.getMetaData(); 320 | int colCount = metaData.getColumnCount(); 321 | List> list = new ArrayList<>(); 322 | 323 | while (rs.next()) { 324 | Map row = new LinkedHashMap<>(); 325 | for (int i = 1; i <= colCount; i++) { 326 | String colName = metaData.getColumnLabel(i); 327 | Object value = rs.getObject(i); 328 | row.put(colName, value); 329 | } 330 | list.add(row); 331 | } 332 | 333 | return list; 334 | } catch (SQLException e) { 335 | throw new Exception("执行 EXPLAIN 失败:" + e.getMessage(), e); 336 | } 337 | } 338 | 339 | 340 | 341 | } 342 | -------------------------------------------------------------------------------- /src/main/java/com/chen/action/ERDiagramToolWindowFactory.java: -------------------------------------------------------------------------------- 1 | package com.chen.action; 2 | 3 | import com.chen.entity.DbConfig; 4 | import com.chen.utils.DataSourceManager; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.wm.ToolWindow; 7 | import com.intellij.openapi.wm.ToolWindowFactory; 8 | import com.intellij.ui.jcef.JBCefBrowser; 9 | import com.intellij.ui.content.Content; 10 | import com.intellij.ui.content.ContentFactory; 11 | 12 | import javax.swing.*; 13 | import java.awt.*; 14 | import java.io.*; 15 | import java.nio.charset.StandardCharsets; 16 | import java.sql.*; 17 | import java.util.*; 18 | import java.util.List; 19 | 20 | import static com.chen.constant.FileConstant.D3_JS_PATH; 21 | import static com.chen.constant.FileConstant.DAGRE_JS_PATH; 22 | import static com.chen.constant.MessageConstants.ERROR_NO_DB_CONFIG; 23 | import static com.chen.constant.MessageConstants.NO_DB_CONFIG_HTML; 24 | import static com.chen.utils.DbConfigUtil.loadFromCache; 25 | 26 | /** 27 | * IDEA 工具窗口:展示数据库ER图(GoJS已换为D3+dagre-d3)。 28 | * 支持一键全部刷新、批量获取表/字段/外键信息,展示全部表及外键关系。 29 | * 支持表名及备注、字段类型及备注的显示。 30 | */ 31 | public class ERDiagramToolWindowFactory implements ToolWindowFactory { 32 | 33 | 34 | /** 刷新按钮文本 */ 35 | private static final String REFRESH_BUTTON_TEXT = "刷新ER图"; 36 | /** 刷新按钮宽度 */ 37 | private static final int REFRESH_BUTTON_WIDTH = 90; 38 | /** 刷新按钮高度 */ 39 | private static final int REFRESH_BUTTON_HEIGHT = 28; 40 | /** 刷新按钮左侧边距 */ 41 | private static final int REFRESH_BUTTON_LEFT_MARGIN = 10; 42 | /** 刷新按钮上侧边距 */ 43 | private static final int REFRESH_BUTTON_TOP_MARGIN = 10; 44 | /** ER图页签标题 */ 45 | private static final String TAB_TITLE = "ER Diagram"; 46 | 47 | @Override 48 | public void createToolWindowContent(Project project, ToolWindow toolWindow) { 49 | // 创建浏览器组件 50 | JBCefBrowser browser = new JBCefBrowser(); 51 | 52 | // 主面板,包含浏览器和按钮 53 | JPanel panel = new JPanel(new BorderLayout()); 54 | panel.add(browser.getComponent(), BorderLayout.CENTER); 55 | 56 | // —— 刷新ER图按钮 —— 57 | JButton refreshButton = new JButton(REFRESH_BUTTON_TEXT); 58 | refreshButton.setFocusable(false); 59 | refreshButton.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); 60 | refreshButton.setPreferredSize(new Dimension(REFRESH_BUTTON_WIDTH, REFRESH_BUTTON_HEIGHT)); 61 | 62 | JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, REFRESH_BUTTON_LEFT_MARGIN, REFRESH_BUTTON_TOP_MARGIN)); 63 | buttonPanel.setOpaque(false); 64 | buttonPanel.add(refreshButton); 65 | panel.add(buttonPanel, BorderLayout.SOUTH); 66 | 67 | // 刷新按钮点击事件 68 | refreshButton.addActionListener(e -> { 69 | DbConfig dbConfigRefresh = loadFromCache(project); 70 | if (dbConfigRefresh == null) { 71 | showWarnDialog(panel, ERROR_NO_DB_CONFIG); 72 | return; 73 | } 74 | try (Connection conn = DataSourceManager.getDataSource(dbConfigRefresh).getConnection()) { 75 | String html = buildERHtml(conn); 76 | browser.loadHTML(html); 77 | showInfoDialog(panel, "ER图刷新成功!"); 78 | } catch (Exception ex) { 79 | ex.printStackTrace(); 80 | browser.loadHTML(buildErrorHtml("数据库连接失败,无法生成ER图。\n" + ex.getMessage())); 81 | } 82 | }); 83 | 84 | // 初始加载(自动加载上次数据库配置) 85 | DbConfig dbConfig = loadFromCache(project); 86 | if (dbConfig == null) { 87 | browser.loadHTML(NO_DB_CONFIG_HTML); 88 | } else { 89 | try (Connection conn = DataSourceManager.getDataSource(dbConfig).getConnection()) { 90 | String html = buildERHtml(conn); 91 | browser.loadHTML(html); 92 | } catch (Exception e) { 93 | e.printStackTrace(); 94 | browser.loadHTML(buildErrorHtml("数据库连接失败,无法生成ER图。\n" + e.getMessage())); 95 | } 96 | } 97 | 98 | // 添加到IntelliJ ToolWindow 99 | ContentFactory contentFactory = ContentFactory.getInstance(); 100 | Content content = contentFactory.createContent(panel, TAB_TITLE, false); 101 | toolWindow.getContentManager().addContent(content); 102 | } 103 | 104 | /** 105 | * 构建ER图HTML内容(批量查询数据库表、字段、外键信息,生成D3+dagre-d3代码)。 106 | * @param conn 有效的MySQL数据库连接 107 | * @return HTML字符串 108 | * @throws SQLException SQL异常 109 | * @throws IOException IO异常(js文件读取) 110 | */ 111 | public String buildERHtml(Connection conn) throws SQLException, IOException { 112 | StringBuilder nodesBuilder = new StringBuilder(); 113 | StringBuilder edgesBuilder = new StringBuilder(); 114 | 115 | String catalog = conn.getCatalog(); 116 | 117 | // 1. 批量查询所有表备注 118 | Map tableRemarks = queryTableRemarks(conn, catalog); 119 | 120 | // 2. 批量查询所有表字段及字段备注 121 | Map> tableFields = queryTableFields(conn, catalog); 122 | 123 | // 3. 批量查询所有外键信息 124 | List> links = queryForeignKeys(conn, catalog); 125 | 126 | // 4. 生成节点(表+备注+字段) 127 | for (Map.Entry> entry : tableFields.entrySet()) { 128 | String table = entry.getKey(); 129 | Map fields = entry.getValue(); 130 | StringBuilder label = new StringBuilder(); 131 | label.append(table); 132 | // 拼接表备注(如 user(用户表)) 133 | if (tableRemarks.containsKey(table) && !tableRemarks.get(table).isEmpty()) { 134 | label.append("(").append(tableRemarks.get(table)).append(")"); 135 | } 136 | label.append("\\n———\\n"); 137 | // 拼接字段及备注 138 | for (Map.Entry field : fields.entrySet()) { 139 | label.append(field.getKey()).append(": ").append(field.getValue()).append("\\n"); 140 | } 141 | nodesBuilder.append("g.setNode(\"").append(table).append("\", {\n") 142 | .append("label: \"").append(label.toString().replace("\"", "\\\"")).append("\",\n") 143 | .append("shape: \"rect\"\n") 144 | .append("});\n"); 145 | } 146 | 147 | // 5. 生成边(外键关系) 148 | for (Map link : links) { 149 | edgesBuilder.append("g.setEdge(\"").append(link.get("from")).append("\", \"") 150 | .append(link.get("to")).append("\", {\n") 151 | .append("label: \"").append(link.get("label")).append("\",\n") 152 | .append("lineInterpolate: \"basis\",\n") 153 | .append("arrowhead: \"vee\"\n") 154 | .append("});\n"); 155 | } 156 | 157 | // 6. 读取d3.js和dagre-d3.js资源 158 | String d3Js = readJsResource(D3_JS_PATH); 159 | String dagreJs = readJsResource(DAGRE_JS_PATH); 160 | return String.format(HTML_TEMPLATE, d3Js, dagreJs, nodesBuilder, edgesBuilder); 161 | } 162 | 163 | /** 164 | * 批量查询所有表备注 165 | * @param conn 数据库连接 166 | * @param catalog 数据库名 167 | * @return 表名 -> 表备注 168 | * @throws SQLException SQL异常 169 | */ 170 | private Map queryTableRemarks(Connection conn, String catalog) throws SQLException { 171 | Map tableRemarks = new HashMap<>(); 172 | final String tableRemarkSql = "SELECT TABLE_NAME, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = ?"; 173 | try (PreparedStatement ps = conn.prepareStatement(tableRemarkSql)) { 174 | ps.setString(1, catalog); 175 | try (ResultSet rs = ps.executeQuery()) { 176 | while (rs.next()) { 177 | String tableName = rs.getString("TABLE_NAME"); 178 | String comment = rs.getString("TABLE_COMMENT"); 179 | tableRemarks.put(tableName, (comment != null && !comment.isBlank()) ? comment : ""); 180 | } 181 | } 182 | } 183 | return tableRemarks; 184 | } 185 | 186 | /** 187 | * 批量查询所有表字段及字段备注 188 | * @param conn 数据库连接 189 | * @param catalog 数据库名 190 | * @return 表名 -> (字段名 -> 类型+备注) 191 | * @throws SQLException SQL异常 192 | */ 193 | private Map> queryTableFields(Connection conn, String catalog) throws SQLException { 194 | Map> tableFields = new LinkedHashMap<>(); 195 | final String columnSql = "SELECT TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ?"; 196 | try (PreparedStatement ps = conn.prepareStatement(columnSql)) { 197 | ps.setString(1, catalog); 198 | try (ResultSet rs = ps.executeQuery()) { 199 | while (rs.next()) { 200 | String tableName = rs.getString("TABLE_NAME"); 201 | String colName = rs.getString("COLUMN_NAME"); 202 | String type = rs.getString("COLUMN_TYPE"); 203 | String remark = rs.getString("COLUMN_COMMENT"); 204 | tableFields 205 | .computeIfAbsent(tableName, k -> new LinkedHashMap<>()) 206 | .put(colName, type + (remark != null && !remark.isEmpty() ? " (" + remark + ")" : "")); 207 | } 208 | } 209 | } 210 | return tableFields; 211 | } 212 | 213 | /** 214 | * 批量查询所有外键信息 215 | * @param conn 数据库连接 216 | * @param catalog 数据库名 217 | * @return 外键关系列表 218 | * @throws SQLException SQL异常 219 | */ 220 | private List> queryForeignKeys(Connection conn, String catalog) throws SQLException { 221 | List> links = new ArrayList<>(); 222 | final String fkSql = "SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME " + 223 | "FROM information_schema.KEY_COLUMN_USAGE " + 224 | "WHERE TABLE_SCHEMA = ? AND REFERENCED_TABLE_NAME IS NOT NULL"; 225 | try (PreparedStatement ps = conn.prepareStatement(fkSql)) { 226 | ps.setString(1, catalog); 227 | try (ResultSet rs = ps.executeQuery()) { 228 | while (rs.next()) { 229 | String pkTable = rs.getString("REFERENCED_TABLE_NAME"); 230 | String pkColumn = rs.getString("REFERENCED_COLUMN_NAME"); 231 | String fkTable = rs.getString("TABLE_NAME"); 232 | String fkColumn = rs.getString("COLUMN_NAME"); 233 | links.add(Map.of( 234 | "from", pkTable, 235 | "to", fkTable, 236 | "label", pkTable + "." + pkColumn + " -> " + fkTable + "." + fkColumn 237 | )); 238 | } 239 | } 240 | } 241 | return links; 242 | } 243 | 244 | /** 245 | * 读取js资源文件内容 246 | * @param resourcePath 资源路径 247 | * @return 文件内容字符串 248 | * @throws IOException IO异常 249 | */ 250 | private String readJsResource(String resourcePath) throws IOException { 251 | // 兼容IDEA插件环境的资源读取 252 | InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath); 253 | if (is == null) throw new FileNotFoundException("资源未找到: " + resourcePath); 254 | try (is) { 255 | return new String(is.readAllBytes(), StandardCharsets.UTF_8); 256 | } 257 | } 258 | 259 | /** 260 | * 构建错误信息的HTML片段 261 | * @param message 错误信息 262 | * @return HTML字符串 263 | */ 264 | private String buildErrorHtml(String message) { 265 | return "

" + message + "
"; 266 | } 267 | 268 | /** 269 | * 弹出警告对话框 270 | * @param parent 父组件 271 | * @param message 提示信息 272 | */ 273 | private void showWarnDialog(Component parent, String message) { 274 | JOptionPane.showMessageDialog(parent, message, "提示", JOptionPane.WARNING_MESSAGE); 275 | } 276 | 277 | /** 278 | * 弹出信息对话框 279 | * @param parent 父组件 280 | * @param message 信息 281 | */ 282 | private void showInfoDialog(Component parent, String message) { 283 | JOptionPane.showMessageDialog(parent, message, "提示", JOptionPane.INFORMATION_MESSAGE); 284 | } 285 | 286 | /** 287 | * ER图HTML模板,内嵌D3与Dagre-D3绘图脚本和样式 288 | */ 289 | private static final String HTML_TEMPLATE = """ 290 | 291 | 292 | 293 | 294 | ER 图 295 | 298 | 301 | 309 | 310 | 311 | 312 | 313 | 314 | 361 | 362 | 363 | """; 364 | } -------------------------------------------------------------------------------- /src/main/java/com/chen/utils/DbConfigUtil.java: -------------------------------------------------------------------------------- 1 | package com.chen.utils; 2 | import com.chen.entity.DbConfig; 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.intellij.openapi.application.ApplicationManager; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.ui.Messages; 8 | import org.yaml.snakeyaml.Yaml; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.io.File; 13 | import java.io.FileInputStream; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.nio.charset.StandardCharsets; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.nio.file.Paths; 20 | import java.util.*; 21 | import java.util.List; 22 | import java.util.function.Consumer; 23 | import java.util.stream.Collectors; 24 | 25 | import static com.chen.constant.DataSourceConstants.DB_TYPE_MYSQL; 26 | import static com.chen.constant.DbConstant.*; 27 | import static com.chen.constant.FileConstant.CONFIG_PATH; 28 | import static com.chen.constant.FileConstant.CONFIG_PATH_ALL; 29 | import static com.chen.constant.MessageConstants.*; 30 | import static com.chen.utils.DataSourceUrlFixer.appendUrlFix; 31 | 32 | /** 33 | * @author czh 34 | * @version 1.0 35 | * @description: 36 | * @date 2025/6/14 7:25 37 | */ 38 | public class DbConfigUtil { 39 | 40 | private static final ObjectMapper objectMapper = new ObjectMapper(); 41 | 42 | /** 43 | * 从项目目录中查找 YAML 配置并解析数据库连接参数,支持 spring.datasource 与 druid.master 配置 44 | */ 45 | public static DbConfig tryLoadDbConfig(Project project) { 46 | List ymlFiles = findAllYmlFiles(new File(project.getBasePath())); 47 | DbConfig dbConfig = null; 48 | 49 | for (File yml : ymlFiles) { 50 | try (InputStream input = new FileInputStream(yml)) { 51 | Yaml yaml = new Yaml(); 52 | Map data = yaml.load(input); 53 | if (data == null) continue; 54 | 55 | @SuppressWarnings("unchecked") 56 | Map spring = (Map) data.get("spring"); 57 | if (spring == null) continue; 58 | 59 | @SuppressWarnings("unchecked") 60 | Map datasource = (Map) spring.get("datasource"); 61 | if (datasource == null) continue; 62 | 63 | // 第一优先:spring.datasource 配置 64 | String url = String.valueOf(datasource.get("url")); 65 | String username = String.valueOf(datasource.get("username")); 66 | String password = String.valueOf(datasource.get("username")); 67 | 68 | if (StringUtils.notBlankAndNotNullStr(url) && StringUtils.notBlankAndNotNullStr(username)) { 69 | dbConfig = new DbConfig(url, username, password); 70 | break; 71 | } 72 | 73 | // 第二优先:druid.master 配置 74 | @SuppressWarnings("unchecked") 75 | Map druid = (Map) datasource.get("druid"); 76 | if (druid == null) continue; 77 | 78 | @SuppressWarnings("unchecked") 79 | Map master = (Map) druid.get("master"); 80 | if (master == null) continue; 81 | 82 | url = String.valueOf(master.get("url")); 83 | username = String.valueOf(master.get("username")); 84 | password = String.valueOf(master.get("password")); 85 | 86 | if (StringUtils.notBlankAndNotNullStr(url) && StringUtils.notBlankAndNotNullStr(username)) { 87 | dbConfig = new DbConfig(url, username, password); 88 | saveToCacheAppend(project, dbConfig); 89 | break; 90 | } 91 | } catch (Exception ignored) { 92 | } 93 | } 94 | 95 | 96 | if (dbConfig != null) { 97 | saveToCacheAppend(project, dbConfig); 98 | saveToCache(project, dbConfig); 99 | } 100 | 101 | return dbConfig; 102 | } 103 | 104 | 105 | /** 106 | * 递归查找所有 application*.yml 配置文件(仅限 src/main/resources) 107 | */ 108 | public static List findAllYmlFiles(File rootDir) { 109 | List result = new ArrayList<>(); 110 | Queue queue = new LinkedList<>(); 111 | queue.add(rootDir); 112 | 113 | while (!queue.isEmpty()) { 114 | File current = queue.poll(); 115 | if (current == null || !current.exists()) continue; 116 | if (current.isDirectory()) { 117 | File[] children = current.listFiles(); 118 | if (children != null) Collections.addAll(queue, children); 119 | } else if (isYmlFile(current)) { 120 | result.add(current); 121 | } 122 | } 123 | return result; 124 | } 125 | 126 | /** 127 | * 判断是否为 application(-xxx)?.yml 文件 128 | */ 129 | public static boolean isYmlFile(File file) { 130 | String name = file.getName(); 131 | return name.matches("application(-[\\w]+)?\\.ya?ml") && 132 | file.getParentFile() != null && 133 | file.getParentFile().getPath().replace("\\", "/").endsWith("src/main/resources"); 134 | } 135 | 136 | 137 | /** 138 | * 弹出异步输入框,直到用户输入完整数据库 URL 和用户名或取消 139 | * 140 | * @param project 当前项目 141 | * @param callback 用户输入完成回调 142 | */ 143 | public static void promptUserInput(Project project, Consumer callback) { 144 | promptUserInputInternal(project, callback); 145 | } 146 | 147 | /** 148 | * 新增数据源 149 | * 150 | * @param project 当前项目 151 | * @param callback 用户输入完成回调 152 | */ 153 | public static void promptUserAdd(Project project, Consumer callback) { 154 | promptUserAddInternal(project, callback); 155 | } 156 | 157 | /** 158 | * 配置默认数据源 159 | * 160 | * @param project 当前项目 161 | * @param callback 用户输入完成回调 162 | */ 163 | public static void promptUserInputWithDbType(Project project, Consumer callback) { 164 | ApplicationManager.getApplication().invokeLater(() -> { 165 | List configs = new ArrayList<>(); 166 | Path path = Paths.get(project.getBasePath(), CONFIG_PATH_ALL); 167 | 168 | // 读取配置文件 169 | if (Files.exists(path)) { 170 | try { 171 | String json = Files.readString(path, StandardCharsets.UTF_8); 172 | if (json != null && !json.isBlank()) { 173 | configs = objectMapper.readValue(json, new TypeReference>() { 174 | }); 175 | } 176 | } catch (IOException e) { 177 | Messages.showErrorDialog(project, "读取数据库配置文件失败: " + e.getMessage(), "错误"); 178 | } 179 | } 180 | 181 | // 分组 182 | Map> typeToConfigs = configs.stream() 183 | .filter(c -> c.getUrl() != null) 184 | .collect(Collectors.groupingBy(c -> parseDbType(c.getUrl()))); 185 | 186 | if (typeToConfigs.isEmpty()) { 187 | typeToConfigs.put("mysql", List.of(new DbConfig(DEFAULT_MYSQL, "", ""))); 188 | } 189 | 190 | // UI组件 191 | String[] dbTypes = typeToConfigs.keySet().toArray(new String[0]); 192 | JComboBox dbTypeComboBox = new JComboBox<>(dbTypes); 193 | JComboBox configComboBox = new JComboBox<>(); 194 | JTextField urlField = new JTextField(); 195 | JTextField usernameField = new JTextField(); 196 | JPasswordField passwordField = new JPasswordField(); 197 | 198 | // 删除按钮(➖) 199 | JButton deleteButton = new JButton("➖"); 200 | deleteButton.setToolTipText("删除当前数据源"); 201 | deleteButton.setPreferredSize(new Dimension(45, 24)); 202 | 203 | // 刷新配置列表下拉框 204 | Consumer refreshConfigList = (isInit) -> { 205 | configComboBox.removeAllItems(); 206 | String selectedType = (String) dbTypeComboBox.getSelectedItem(); 207 | if (selectedType != null && typeToConfigs.containsKey(selectedType)) { 208 | List list = typeToConfigs.get(selectedType); 209 | for (DbConfig cfg : list) { 210 | configComboBox.addItem(cfg.getUrl()); 211 | } 212 | 213 | if (!list.isEmpty()) { 214 | if (isInit) { 215 | DbConfig dbConfig = loadFromCache(project); 216 | if (dbConfig != null) { 217 | configComboBox.setSelectedItem(dbConfig.getUrl()); 218 | urlField.setText(dbConfig.getUrl()); 219 | usernameField.setText(dbConfig.getUsername()); 220 | passwordField.setText(dbConfig.getPassword()); 221 | } 222 | } else { 223 | configComboBox.setSelectedIndex(0); 224 | DbConfig first = list.get(0); 225 | urlField.setText(first.getUrl()); 226 | usernameField.setText(first.getUsername()); 227 | passwordField.setText(first.getPassword()); 228 | } 229 | } 230 | } 231 | }; 232 | 233 | // 删除按钮逻辑 234 | deleteButton.addActionListener(e -> { 235 | String selectedType = (String) dbTypeComboBox.getSelectedItem(); 236 | int selectedIndex = configComboBox.getSelectedIndex(); 237 | 238 | if (selectedType != null && selectedIndex >= 0 && typeToConfigs.containsKey(selectedType)) { 239 | List list = typeToConfigs.get(selectedType); 240 | if (selectedIndex >= list.size()) return; 241 | DbConfig toRemove = list.get(selectedIndex); 242 | 243 | int confirm = JOptionPane.showConfirmDialog(null, 244 | "确认删除该数据源配置?\n" + toRemove.getUrl(), 245 | "确认删除", JOptionPane.YES_NO_OPTION); 246 | 247 | if (confirm == JOptionPane.YES_OPTION) { 248 | list.remove(selectedIndex); 249 | 250 | // 写回 JSON 文件 251 | try { 252 | List allConfigs = typeToConfigs.values().stream() 253 | .flatMap(Collection::stream).collect(Collectors.toList()); 254 | String newJson = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(allConfigs); 255 | Files.writeString(path, newJson, StandardCharsets.UTF_8); 256 | Messages.showInfoMessage(project, DELETE_SUCCESS_MESSAGE, DELETE_SUCCESS_TITLE); 257 | } catch (IOException ex) { 258 | Messages.showErrorDialog(project, "写入配置文件失败: " + ex.getMessage(), SQL_ERROR_TITLE); 259 | } 260 | 261 | refreshConfigList.accept(false); 262 | } 263 | } 264 | }); 265 | 266 | // 监听选择变化 267 | dbTypeComboBox.addActionListener(e -> refreshConfigList.accept(false)); 268 | configComboBox.addActionListener(e -> { 269 | String selectedType = (String) dbTypeComboBox.getSelectedItem(); 270 | int idx = configComboBox.getSelectedIndex(); 271 | if (selectedType != null && idx >= 0 && typeToConfigs.containsKey(selectedType)) { 272 | List list = typeToConfigs.get(selectedType); 273 | if (idx < list.size()) { 274 | DbConfig selected = list.get(idx); 275 | urlField.setText(selected.getUrl()); 276 | usernameField.setText(selected.getUsername()); 277 | passwordField.setText(selected.getPassword()); 278 | } 279 | } 280 | }); 281 | 282 | // 初始化 283 | DbConfig dbConfig = loadFromCache(project); 284 | if(dbConfig == null) { 285 | dbConfig= configs.isEmpty() ? null : 286 | configs.get(new Random().nextInt(configs.size())); 287 | saveToCache(project, dbConfig); 288 | } 289 | if (dbConfig == null) { 290 | JOptionPane.showMessageDialog(null, "请先添加数据源!", "提示", JOptionPane.WARNING_MESSAGE); 291 | } 292 | String dataBase = parseDbType(dbConfig.getUrl()); 293 | dbTypeComboBox.setSelectedItem(dataBase); 294 | refreshConfigList.accept(true); 295 | 296 | 297 | // 构造 UI 面板 298 | JPanel panel = new JPanel(new GridLayout(0, 1)); 299 | panel.setPreferredSize(new Dimension(500, 300)); 300 | panel.add(new JLabel("数据库类型:")); 301 | panel.add(dbTypeComboBox); 302 | panel.add(new JLabel("具体配置:")); 303 | 304 | // 下拉框和删除按钮放一行 305 | JPanel configLine = new JPanel(new BorderLayout()); 306 | configLine.add(configComboBox, BorderLayout.CENTER); 307 | configLine.add(deleteButton, BorderLayout.EAST); 308 | panel.add(configLine); 309 | 310 | panel.add(new JLabel("数据库 URL:")); 311 | panel.add(urlField); 312 | panel.add(new JLabel("用户名:")); 313 | panel.add(usernameField); 314 | panel.add(new JLabel("密码:")); 315 | panel.add(passwordField); 316 | 317 | // 循环弹窗 318 | while (true) { 319 | int result = JOptionPane.showConfirmDialog( 320 | null, 321 | panel, 322 | "数据库配置", 323 | JOptionPane.OK_CANCEL_OPTION, 324 | JOptionPane.PLAIN_MESSAGE 325 | ); 326 | 327 | if (result != JOptionPane.OK_OPTION) break; 328 | 329 | String dbType = ((String) dbTypeComboBox.getSelectedItem()).toLowerCase(); 330 | String url = urlField.getText().trim(); 331 | String username = usernameField.getText().trim(); 332 | String password = new String(passwordField.getPassword()).trim(); 333 | 334 | List errorList = new ArrayList<>(); 335 | if (!StringUtils.notBlankAndNotNullStr(url)) errorList.add("数据库 URL"); 336 | if (!StringUtils.notBlankAndNotNullStr(username)) errorList.add("用户名"); 337 | if (!StringUtils.notBlankAndNotNullStr(dbType)) errorList.add("数据库类型"); 338 | 339 | if (errorList.isEmpty()) { 340 | callback.accept(new DbConfig(url, username, password)); 341 | break; 342 | } else { 343 | Messages.showErrorDialog(project, 344 | "以下字段不能为空:\n" + String.join("、", errorList), 345 | "输入不完整"); 346 | } 347 | } 348 | }); 349 | } 350 | 351 | 352 | private static void promptUserInputInternal(Project project, Consumer callback) { 353 | ApplicationManager.getApplication().invokeLater(() -> { 354 | JTextField urlField = new JTextField(DEFAULT_MYSQL); 355 | JTextField usernameField = new JTextField(); 356 | JPasswordField passwordField = new JPasswordField(); 357 | 358 | JPanel panel = new JPanel(new GridLayout(0, 1)); 359 | panel.setPreferredSize(new Dimension(300, 200)); 360 | panel.add(new JLabel("数据库 URL:")); 361 | panel.add(urlField); 362 | panel.add(new JLabel("用户名:")); 363 | panel.add(usernameField); 364 | panel.add(new JLabel("密码:")); 365 | panel.add(passwordField); 366 | 367 | 368 | int result = JOptionPane.showConfirmDialog( 369 | null, 370 | panel, 371 | "请输入数据库配置", 372 | JOptionPane.OK_CANCEL_OPTION, 373 | JOptionPane.PLAIN_MESSAGE 374 | ); 375 | 376 | 377 | if (result != JOptionPane.OK_OPTION) { 378 | // 用户点击了“取消”或关闭了窗口,直接返回,不继续执行下面的逻辑 379 | return; 380 | } 381 | String url = urlField.getText().trim(); 382 | String username = usernameField.getText().trim(); 383 | String password = new String(passwordField.getPassword()).trim(); 384 | 385 | List errorList = new ArrayList<>(); 386 | if (!StringUtils.notBlankAndNotNullStr(url)) { 387 | errorList.add("数据库 URL"); 388 | } 389 | if (!StringUtils.notBlankAndNotNullStr(username)) { 390 | errorList.add("用户名"); 391 | } 392 | 393 | if (errorList.isEmpty()) { 394 | url += appendUrlFix(parseDbType(url), url); 395 | callback.accept(new DbConfig(url, username, password)); 396 | saveToCache(project, new DbConfig(url, username, password)); 397 | saveToCacheAppend(project, new DbConfig(url, username, password)); 398 | } else { 399 | Messages.showErrorDialog(project, 400 | "以下字段不能为空:\n" + String.join("、", errorList), 401 | "输入不完整"); 402 | } 403 | 404 | }); 405 | } 406 | 407 | 408 | private static void promptUserAddInternal(Project project, Consumer callback) { 409 | ApplicationManager.getApplication().invokeLater(() -> { 410 | JTextField urlField = new JTextField(DEFAULT_MYSQL); 411 | JTextField usernameField = new JTextField(); 412 | JPasswordField passwordField = new JPasswordField(); 413 | 414 | JPanel panel = new JPanel(new GridLayout(0, 1)); 415 | panel.setPreferredSize(new Dimension(300, 200)); 416 | panel.add(new JLabel("数据库 URL:")); 417 | panel.add(urlField); 418 | panel.add(new JLabel("用户名:")); 419 | panel.add(usernameField); 420 | panel.add(new JLabel("密码:")); 421 | panel.add(passwordField); 422 | 423 | int result = JOptionPane.showConfirmDialog( 424 | null, 425 | panel, 426 | "请输入数据库配置", 427 | JOptionPane.OK_CANCEL_OPTION, 428 | JOptionPane.PLAIN_MESSAGE 429 | ); 430 | if (result != JOptionPane.OK_OPTION) { 431 | // 用户点击了“取消”或关闭了窗口,直接返回,不继续执行下面的逻辑 432 | return; 433 | } 434 | String url = urlField.getText().trim(); 435 | String username = usernameField.getText().trim(); 436 | String password = new String(passwordField.getPassword()).trim(); 437 | 438 | List errorList = new ArrayList<>(); 439 | if (!StringUtils.notBlankAndNotNullStr(url)) { 440 | errorList.add("数据库 URL"); 441 | } 442 | if (!StringUtils.notBlankAndNotNullStr(username)) { 443 | errorList.add("用户名"); 444 | } 445 | 446 | if (errorList.isEmpty()) { 447 | url += appendUrlFix(parseDbType(url), url); 448 | callback.accept(new DbConfig(url, username, password)); 449 | saveToCache(project, new DbConfig(url, username, password)); 450 | saveToCacheAppend(project, new DbConfig(url, username, password)); 451 | } else { 452 | Messages.showErrorDialog(project, 453 | "以下字段不能为空:\n" + String.join("、", errorList), 454 | "输入不完整"); 455 | } 456 | 457 | }); 458 | } 459 | 460 | 461 | /** 462 | * 弹出同步阻塞输入框,直到用户输入完整数据库 URL 和用户名或取消 463 | * 464 | * @param project 当前项目 465 | * @return 用户输入的数据库配置,用户取消返回 null 466 | */ 467 | public static DbConfig promptUserInputSync(Project project) { 468 | JTextField urlField = new JTextField(DEFAULT_MYSQL); 469 | JTextField usernameField = new JTextField(); 470 | JPasswordField passwordField = new JPasswordField(); 471 | 472 | JPanel panel = new JPanel(new GridLayout(0, 1)); 473 | panel.setPreferredSize(new Dimension(300, 200)); 474 | panel.add(new JLabel("数据库 URL:")); 475 | panel.add(urlField); 476 | panel.add(new JLabel("用户名:")); 477 | panel.add(usernameField); 478 | panel.add(new JLabel("密码:")); 479 | panel.add(passwordField); 480 | 481 | while (true) { 482 | int result = JOptionPane.showConfirmDialog( 483 | null, 484 | panel, 485 | "请输入数据库配置", 486 | JOptionPane.OK_CANCEL_OPTION, 487 | JOptionPane.PLAIN_MESSAGE 488 | ); 489 | 490 | if (result != JOptionPane.OK_OPTION) { 491 | return null; // 用户取消 492 | } 493 | 494 | String url = urlField.getText().trim(); 495 | String username = usernameField.getText().trim(); 496 | String password = new String(passwordField.getPassword()).trim(); 497 | 498 | List errorList = new ArrayList<>(); 499 | if (!StringUtils.notBlankAndNotNullStr(url)) { 500 | errorList.add("数据库 URL"); 501 | } 502 | if (!StringUtils.notBlankAndNotNullStr(username)) { 503 | errorList.add("用户名"); 504 | } 505 | 506 | if (errorList.isEmpty()) { 507 | url += appendUrlFix(parseDbType(url), url); 508 | saveToCache(project, new DbConfig(url, username, password)); 509 | saveToCacheAppend(project, new DbConfig(url, username, password)); 510 | return new DbConfig(url, username, password); 511 | } else { 512 | Messages.showErrorDialog(project, 513 | "以下字段不能为空:\n" + String.join("、", errorList), 514 | "输入不完整"); 515 | } 516 | } 517 | } 518 | 519 | 520 | /** 521 | * 保存数据库配置到缓存文件,写成 JSON 格式,并测试数据库连接有效性 522 | * 523 | * @param project 当前项目 524 | * @param config 数据库配置对象 525 | */ 526 | public static boolean saveToCache(Project project, DbConfig config) { 527 | Path path = Paths.get(project.getBasePath(), CONFIG_PATH); 528 | 529 | try { 530 | // 优先判断:如果已有缓存且内容一致,则跳过所有操作,提升性能 531 | if (Files.exists(path)) { 532 | String oldJson = Files.readString(path, StandardCharsets.UTF_8); 533 | if (oldJson == null || oldJson.trim().isEmpty()) { 534 | // 直接写入新的配置,跳过解析 535 | String newJson = objectMapper.writeValueAsString(config); 536 | Files.writeString(path, newJson, StandardCharsets.UTF_8); 537 | return true; 538 | } 539 | DbConfig oldConfig = objectMapper.readValue(oldJson, DbConfig.class); 540 | if (Objects.equals(oldConfig, config)) { 541 | String dbType = parseDbType(config.getUrl()); 542 | if (DB_TYPE_MYSQL.equalsIgnoreCase(dbType)) { 543 | String yamlTemplate = SoarYamlUtil.readYamlTemplate("soar.yaml"); 544 | String replacedYaml = SoarYamlUtil.replaceDbConfigInYaml(config, yamlTemplate); 545 | Path ideaDir = Paths.get(project.getBasePath(), ".idea"); 546 | SoarYamlUtil.copySoarExeToDir(ideaDir); 547 | String userProjectPath = project.getBasePath(); 548 | SoarYamlUtil.writeYamlToProjectIdea(userProjectPath, replacedYaml); 549 | } 550 | // 配置一致,无需保存 551 | return true; 552 | } 553 | } 554 | 555 | // 配置有变化,先测试连接 556 | if (config != null && !JdbcTableInfoUtil.testConnection(config)) { 557 | return false; 558 | } 559 | 560 | // 写入新配置 561 | Files.createDirectories(path.getParent()); 562 | String json = objectMapper.writeValueAsString(config); 563 | Files.writeString(path, json, StandardCharsets.UTF_8); 564 | String dbType = parseDbType(config.getUrl()); 565 | if (DB_TYPE_MYSQL.equalsIgnoreCase(dbType)) { 566 | String yamlTemplate = SoarYamlUtil.readYamlTemplate("soar.yaml"); 567 | String replacedYaml = SoarYamlUtil.replaceDbConfigInYaml(config, yamlTemplate); 568 | Path ideaDir = Paths.get(project.getBasePath(), ".idea"); 569 | SoarYamlUtil.copySoarExeToDir(ideaDir); 570 | String userProjectPath = project.getBasePath(); 571 | SoarYamlUtil.writeYamlToProjectIdea(userProjectPath, replacedYaml); 572 | } 573 | return true; 574 | 575 | } catch (Exception e) { 576 | 577 | return false; 578 | } 579 | } 580 | public static void updateYamlConfig(String yamlPath, DbConfig config) throws IOException { 581 | String yaml = Files.readString(Paths.get(yamlPath)); 582 | 583 | // 假设你能从 config 中拿到 url, user, password 584 | String url = config.getUrl(); 585 | String user = config.getUsername(); 586 | String password = config.getPassword(); 587 | 588 | // 解析出 addr 和 schema(示例逻辑) 589 | String addr = url.substring(url.indexOf("//") + 2, url.indexOf("/", url.indexOf("//") + 2)); 590 | String schema = url.substring(url.indexOf("/", url.indexOf("//") + 2) + 1, 591 | url.contains("?") ? url.indexOf("?") : url.length()); 592 | 593 | // 替换 YAML 内容(保留缩进) 594 | yaml = yaml.replaceAll("(?m)^([ \\t]*)addr:\\s*.*", "$1addr: " + addr); 595 | yaml = yaml.replaceAll("(?m)^([ \\t]*)schema:\\s*.*", "$1schema: " + schema); 596 | yaml = yaml.replaceAll("(?m)^([ \\t]*)user:\\s*.*", "$1user: " + user); 597 | yaml = yaml.replaceAll("(?m)^([ \\t]*)password:\\s*.*", "$1password: " + password); 598 | 599 | // 写回文件 600 | Files.writeString(Paths.get(yamlPath), yaml); 601 | } 602 | 603 | public static boolean saveToCacheAppend(Project project, DbConfig newConfig) { 604 | Path path = Paths.get(project.getBasePath(), CONFIG_PATH_ALL); 605 | try { 606 | List configList = new ArrayList<>(); 607 | 608 | // 读取已有配置列表 609 | if (Files.exists(path)) { 610 | String json = Files.readString(path, StandardCharsets.UTF_8); 611 | if (json != null && !json.isBlank()) { 612 | // 反序列化成列表 613 | configList = objectMapper.readValue(json, new TypeReference>() { 614 | }); 615 | } 616 | } 617 | 618 | // 判断是否已有相同配置,避免重复添加(可根据需要定义相等逻辑) 619 | boolean exists = configList.stream().anyMatch(c -> c.equals(newConfig)); 620 | if (exists) { 621 | return true; // 已存在,不重复添加 622 | } 623 | 624 | // 测试新配置连接 625 | if (!JdbcTableInfoUtil.testConnection(newConfig)) { 626 | return false; 627 | } 628 | 629 | // 添加新配置 630 | configList.add(newConfig); 631 | 632 | // 保存回文件 633 | Files.createDirectories(path.getParent()); 634 | String newJson = objectMapper.writeValueAsString(configList); 635 | Files.writeString(path, newJson, StandardCharsets.UTF_8); 636 | 637 | return true; 638 | } catch (Exception e) { 639 | return false; 640 | } 641 | } 642 | 643 | 644 | /** 645 | * 从缓存文件中读取数据库配置(JSON格式) 646 | * 647 | * @param project 当前项目 648 | * @return 配置对象,读取失败或文件不存在返回 null 649 | */ 650 | public static DbConfig loadFromCache(Project project) { 651 | try { 652 | Path path = Paths.get(project.getBasePath(), CONFIG_PATH); 653 | if (!Files.exists(path)) return null; 654 | String json = Files.readString(path, StandardCharsets.UTF_8); 655 | if (json.isEmpty()) { 656 | return null; 657 | } 658 | return objectMapper.readValue(json, DbConfig.class); 659 | } catch (Exception ignored) { 660 | ignored.printStackTrace(); 661 | } 662 | return null; 663 | } 664 | 665 | public static String parseDbType(String url) { 666 | if (url == null || !url.startsWith("jdbc:")) { 667 | throw new IllegalArgumentException("无效的 JDBC URL: " + url); 668 | } 669 | return url.split(":")[1].toLowerCase(); 670 | } 671 | public static String removeUrlFix(String url) { 672 | int idx = url.indexOf("?"); 673 | return (idx != -1) ? url.substring(0, idx) : url; 674 | } 675 | } 676 | --------------------------------------------------------------------------------