├── .gitignore ├── .travis.yml ├── README.md ├── pom.xml └── src ├── main └── java │ └── net │ └── moznion │ └── mysql │ └── diff │ ├── App.java │ ├── DiffExtractor.java │ ├── MySqlConnectionInfo.java │ ├── MySqlConnectionUri.java │ ├── SchemaDumper.java │ ├── SchemaParser.java │ └── model │ ├── Column.java │ ├── OrdinaryKey.java │ ├── Table.java │ └── UniqueKey.java └── test └── java └── net └── moznion └── mysql └── diff ├── AppTest.java ├── ConnectionInfoTest.java ├── DiffExtractorTest.java └── SchemaDumperTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io 2 | 3 | ### Java ### 4 | *.class 5 | 6 | # Mobile Tools for Java (J2ME) 7 | .mtj.tmp/ 8 | 9 | # Package Files # 10 | *.jar 11 | *.war 12 | *.ear 13 | 14 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 15 | hs_err_pid* 16 | 17 | /target/ 18 | 19 | /.checkstyle 20 | /.classpath 21 | /.project 22 | /.settings/ 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | install: mvn install -DskipTests=true -Dgpg.skip=true 5 | before_deploy: 6 | - mvn -P fatjar clean package -DskipTests=true -Dproject.finalName=$TRAVIS_TAG 7 | deploy: 8 | provider: releases 9 | api_key: 10 | secure: tBjY19aiHZ02GsCj9W+B0iscsZM8X+geHuErZdQ82wrhsf9DwxwBqPDYlfVtf19VhbnfZy/JjqMqv+R8T6lIYZxsgvbTaT41IA7dR9g49W6LfPZStY4blYF8cR+WB7tlS7bJ+9KhIzZ/5+DxAqZ78PfDENhMseWMNR39g/d3Dmw= 11 | file: target/$TRAVIS_TAG.jar 12 | skip_cleanup: true 13 | on: 14 | repo: moznion/java-mysql-diff 15 | all_branches: true 16 | tags: true 17 | 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mysql-diff [![Build Status](https://travis-ci.org/moznion/java-mysql-diff.svg?branch=master)](https://travis-ci.org/moznion/java-mysql-diff) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.moznion/mysql-diff/badge.svg)](https://maven-badges.herokuapp.com/maven-central/net.moznion/mysql-diff) [![javadoc.io](https://javadocio-badges.herokuapp.com/net.moznion/mysql-diff/badge.svg)](https://javadocio-badges.herokuapp.com/net.moznion/mysql-diff) 2 | == 3 | 4 | Detect and extract diff between two table declarations from schema of MySQL. 5 | 6 | Synopsis 7 | -- 8 | 9 | ### Use as CLI application 10 | 11 | Executable fat-jar is available at [here](https://github.com/moznion/java-mysql-diff/releases). 12 | 13 | ``` 14 | $ java -jar [old_database] [new_database] 15 | ``` 16 | 17 | If you want more details, please run this command with `--help` option. 18 | 19 | ### Programmatically 20 | 21 | ```java 22 | import net.moznion.mysql.diff.DiffExtractor; 23 | import net.moznion.mysql.diff.MySqlConnectionInfo; 24 | import net.moznion.mysql.diff.SchemaDumper; 25 | import net.moznion.mysql.diff.SchemaParser; 26 | 27 | MySqlConnectionInfo localMySqlConnectionInfo = MySqlConnectionInfo.builder() 28 | // .host("your-host") // "localhost" is the default value 29 | // .user("your-name") // "root" is the default value 30 | // .pass("your-password") // "" is the default value 31 | // .url("jdbc:mysql://your-host:3306?cacheServerConfiguration=true") // or you can specify host, port and properties by a URL 32 | .build(); 33 | SchemaDumper schemaDumper = new SchemaDumper(localMySqlConnectionInfo); 34 | 35 | final String oldSql = "CREATE TABLE `sample` (\n" 36 | + " `id` INTEGER(10) NOT NULL AUTO_INCREMENT,\n" 37 | + " PRIMARY KEY (`id`)\n" 38 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;\n"; 39 | final String newSql = "CREATE TABLE `sample` (\n" 40 | + " `id` INTEGER(10) NOT NULL AUTO_INCREMENT,\n" 41 | + " `name` VARCHAR(32) NOT NULL,\n" 42 | + " PRIMARY KEY (`id`)\n" 43 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;\n"; 44 | 45 | String oldSchema = schemaDumper.dump(oldSql); 46 | String newSchema = schemaDumper.dump(newSql); 47 | 48 | List oldTables = SchemaParser.parse(oldSchema); 49 | List
newTables = SchemaParser.parse(newSchema); 50 | 51 | String diff = DiffExtractor.extractDiff(oldTables, newTables); 52 | ``` 53 | 54 | Description 55 | -- 56 | 57 | This package provides a function to detect and extract diff between two table declarations from schema. 58 | 59 | Extraction processing flow is following; 60 | 61 | 1. Dump normalized schema by creating new database according to input and dump it out by using `mysqldump` 62 | 2. Parse normalized schema 63 | 3. Take diff between parsed structure 64 | 65 | This package is port of onishi-san's [mysqldiff](https://github.com/onishi/mysqldiff) from Perl to Java. 66 | 67 | Dependencies 68 | -- 69 | 70 | - Java 8 or later 71 | - MySQL 5 or later (`mysqld` must be upped when this program is executed) 72 | - mysqldump 73 | 74 | How to build fat-jar 75 | -- 76 | 77 | If you want to build an executable standalone jar, 78 | please run following command; 79 | 80 | ``` 81 | $ mvn -P fatjar clean package 82 | ``` 83 | 84 | And now, generated runnable fat-jar file has been available on [GitHub Releases](https://github.com/moznion/java-mysql-diff/releases). 85 | 86 | See Also 87 | -- 88 | 89 | - [mysqldiff](https://github.com/onishi/mysqldiff) 90 | 91 | Author 92 | -- 93 | 94 | moznion () 95 | 96 | License 97 | -- 98 | 99 | ``` 100 | The MIT License (MIT) 101 | Copyright © 2014 moznion, http://moznion.net/ 102 | 103 | Permission is hereby granted, free of charge, to any person obtaining a copy 104 | of this software and associated documentation files (the “Software”), to deal 105 | in the Software without restriction, including without limitation the rights 106 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 107 | copies of the Software, and to permit persons to whom the Software is 108 | furnished to do so, subject to the following conditions: 109 | 110 | The above copyright notice and this permission notice shall be included in 111 | all copies or substantial portions of the Software. 112 | 113 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 114 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 115 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 116 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 117 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 118 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 119 | THE SOFTWARE. 120 | ``` 121 | 122 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | net.moznion 5 | mysql-diff 6 | 1.1.1-SNAPSHOT 7 | jar 8 | 9 | mysql-diff 10 | Detect and extract diff between two table declarations from schema of MySQL 11 | https://github.com/moznion/java-mysql-diff 12 | 13 | 14 | UTF-8 15 | UTF-8 16 | ${project.artifactId}-${project.version} 17 | 18 | 19 | 20 | 21 | mysql 22 | mysql-connector-java 23 | [8.0.21,) 24 | 25 | 26 | args4j 27 | args4j 28 | 2.0.29 29 | 30 | 31 | junit 32 | junit 33 | 4.13.1 34 | test 35 | 36 | 37 | org.projectlombok 38 | lombok 39 | 1.14.8 40 | provided 41 | 42 | 43 | com.google.code.findbugs 44 | findbugs 45 | 3.0.0 46 | test 47 | 48 | 49 | 50 | 51 | 52 | 53 | org.apache.maven.plugins 54 | maven-compiler-plugin 55 | 3.1 56 | 57 | 1.8 58 | 1.8 59 | 1.8 60 | UTF-8 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-jar-plugin 67 | 2.5 68 | 69 | 70 | 71 | net.moznion.mysql.diff.App 72 | true 73 | true 74 | 75 | 76 | ${project.finalName} 77 | 78 | 79 | 80 | 81 | org.apache.maven.plugins 82 | maven-release-plugin 83 | 2.5.1 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-source-plugin 88 | 2.4 89 | 90 | 91 | attach-sources 92 | 93 | jar 94 | 95 | 96 | 97 | 98 | 99 | org.apache.maven.plugins 100 | maven-javadoc-plugin 101 | 2.10.1 102 | 103 | 104 | attach-javadocs 105 | 106 | jar 107 | 108 | 109 | 110 | 111 | 112 | org.apache.maven.plugins 113 | maven-surefire-plugin 114 | 2.18 115 | 116 | 117 | org.slf4j.simpleLogger.defaultLogLevel 118 | debug 119 | 120 | 121 | 122 | 123 | org.sonatype.plugins 124 | nexus-staging-maven-plugin 125 | 1.6.5 126 | true 127 | 128 | ossrh 129 | https://oss.sonatype.org/ 130 | true 131 | 132 | 133 | 134 | org.apache.maven.plugins 135 | maven-gpg-plugin 136 | 1.5 137 | 138 | 139 | sign-artifacts 140 | verify 141 | 142 | sign 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | fatjar 153 | 154 | 155 | 156 | org.apache.maven.plugins 157 | maven-assembly-plugin 158 | 2.5.3 159 | 160 | 161 | jar-with-dependencies 162 | 163 | false 164 | 165 | 166 | net.moznion.mysql.diff.App 167 | true 168 | true 169 | 170 | 171 | ${project.finalName} 172 | 173 | 174 | 175 | make-assembly 176 | package 177 | 178 | single 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | ${project.artifactId} 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | org.codehaus.mojo 195 | findbugs-maven-plugin 196 | 3.0.0 197 | 198 | 199 | 200 | 201 | 202 | 203 | ossrh 204 | https://oss.sonatype.org/content/repositories/snapshots 205 | 206 | 207 | ossrh 208 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 209 | 210 | 211 | 212 | 213 | https://github.com/moznion/java-mysql-diff 214 | scm:git:git@github.com:moznion/java-mysql-diff.git 215 | scm:git:git@github.com:moznion/java-mysql-diff.git 216 | HEAD 217 | 218 | 219 | 220 | 221 | MIT License 222 | http://www.opensource.org/licenses/mit-license.php 223 | repo 224 | 225 | 226 | 227 | 228 | 229 | moznion 230 | Taiki Kawakami 231 | moznion@gmail.com 232 | http://moznion.net 233 | 234 | 235 | 236 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/App.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import lombok.Getter; 4 | 5 | import net.moznion.mysql.diff.model.Table; 6 | 7 | import org.kohsuke.args4j.Argument; 8 | import org.kohsuke.args4j.CmdLineException; 9 | import org.kohsuke.args4j.CmdLineParser; 10 | import org.kohsuke.args4j.Option; 11 | import org.kohsuke.args4j.spi.StringArrayOptionHandler; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.sql.SQLException; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.Optional; 20 | 21 | /** 22 | * Class for CLI application. 23 | * 24 | * @author moznion 25 | * 26 | */ 27 | public class App { 28 | @Option(name = "-v", aliases = "--version", usage = "print version") 29 | private boolean showVersion; 30 | 31 | @Option(name = "-h", aliases = "--help", usage = "print usage message and exit") 32 | private boolean showUsage; 33 | 34 | @Argument(index = 0, metaVar = "arguments...", handler = StringArrayOptionHandler.class) 35 | private String[] arguments; 36 | 37 | /** 38 | * Class to parse options for remote DB connection information. 39 | */ 40 | @Getter 41 | private class RemoteDbArg { 42 | @Option(name = "-h", aliases = "--host", metaVar = "host", usage = "specify host") 43 | private String host; 44 | 45 | @Option(name = "-u", aliases = "--user", metaVar = "user", usage = "specify user") 46 | private String user; 47 | 48 | @Option(name = "-p", aliases = "--password", metaVar = "pass", usage = "specify password") 49 | private String pass; 50 | 51 | @Argument(index = 0, metaVar = "dbName") 52 | private String dbName; 53 | } 54 | 55 | /** 56 | * Entry point of CLI application. 57 | * 58 | *
 59 |    * [Usage]
 60 |    *     java -jar [old_database] [new_database]
 61 |    * [Examples]
 62 |    * Take diff between createtable1.sql and createtable2.sql (both of SQL files on your machine)
 63 |    *     java -jar createtable1.sql createtable2.sql
 64 |    * Take diff between dbname1 and dbname2 (both of databases on the local MySQL)
 65 |    *     java -jar dbname1 dbname2
 66 |    * Take diff between dbname1 and dbname2 (both of databases on remote MySQL)
 67 |    *     java -jar '-uroot -hlocalhost dbname1' '-uroot -hlocalhost dbname2'
 68 |    * [Options]
 69 |    *     -h, --help:    Show usage
 70 |    *     -v, --version: Show version
 71 |    * 
72 | * 73 | * @param args Options, or target of database arguments. 74 | * @throws IOException Throw if mysqldump command is failed. 75 | * @throws SQLException Throw if invalid SQL is given. 76 | * @throws InterruptedException Throw if mysqldump command is failed. 77 | */ 78 | public static void main(String[] args) throws IOException, SQLException, InterruptedException { 79 | App app = new App(); 80 | 81 | CmdLineParser parser = new CmdLineParser(app); 82 | try { 83 | parser.parseArgument(args); 84 | } catch (CmdLineException e) { 85 | throw new IllegalArgumentException("Invalid arguments are detected: " + Arrays.asList(args)); 86 | } 87 | 88 | if (app.showVersion) { 89 | System.out.println(Optional.ofNullable(App.class.getPackage().getImplementationVersion()) 90 | .orElse("Missing Version")); // XXX "Missing Version" maybe used by testing only... 91 | return; 92 | } 93 | 94 | if (app.showUsage) { 95 | System.out.println(getUsageMessage()); 96 | return; 97 | } 98 | 99 | List coreArgs = Arrays.asList(Optional.ofNullable(args).orElse(new String[0])); 100 | int numOfArgs = coreArgs.size(); 101 | if (numOfArgs != 2) { 102 | if (numOfArgs < 2) { 103 | System.err.println("[ERROR] Too few command line arguments"); 104 | } else { 105 | System.err.println("[ERROR] Too many command line arguments"); 106 | } 107 | System.err.println(); 108 | System.err.println(getUsageMessage()); 109 | System.exit(1); 110 | } 111 | 112 | List> parsed = new ArrayList<>(); 113 | for (String arg : coreArgs) { 114 | String schema; 115 | SchemaDumper schemaDumper = new SchemaDumper(); // TODO should be more configurable 116 | 117 | File file = new File(arg); 118 | if (file.exists()) { 119 | // for file 120 | schema = schemaDumper.dump(file); 121 | } else if (arg.contains(" ")) { 122 | // for remote server 123 | RemoteDbArg remoteDbArg = new App().new RemoteDbArg(); 124 | CmdLineParser remoteDbArgParser = new CmdLineParser(remoteDbArg); 125 | try { 126 | remoteDbArgParser.parseArgument(arg.substring(1, arg.length() - 1).split(" ")); 127 | } catch (CmdLineException e) { 128 | throw new IllegalArgumentException("Invalid remote DB argument is detected: " + arg); 129 | } 130 | 131 | if (remoteDbArg.dbName == null || remoteDbArg.dbName.isEmpty()) { 132 | throw new IllegalArgumentException("Invalid remote DB argument is detected: " + arg); 133 | } 134 | 135 | MySqlConnectionInfo.Builder mysqlConnectionInfoBuilder = MySqlConnectionInfo.builder(); 136 | 137 | if (remoteDbArg.host != null) { 138 | mysqlConnectionInfoBuilder.host(remoteDbArg.host); 139 | } 140 | 141 | if (remoteDbArg.user != null) { 142 | mysqlConnectionInfoBuilder.user(remoteDbArg.user); 143 | } 144 | 145 | if (remoteDbArg.pass != null) { 146 | mysqlConnectionInfoBuilder.pass(remoteDbArg.pass); 147 | } 148 | 149 | schema = 150 | schemaDumper.dumpFromRemoteDb(remoteDbArg.dbName, mysqlConnectionInfoBuilder.build()); 151 | } else { 152 | // for local server 153 | schema = schemaDumper.dumpFromLocalDb(arg); 154 | } 155 | 156 | parsed.add(SchemaParser.parse(schema)); 157 | } 158 | 159 | String diff = DiffExtractor.extractDiff(parsed.get(0), parsed.get(1)); 160 | System.out.println(diff); 161 | } 162 | 163 | private static String getUsageMessage() { 164 | return "[Usage]\n" 165 | + " java -jar [old_database] [new_database]\n" 166 | + "[Examples]\n" 167 | + "* Take diff between createtable1.sql and createtable2.sql " 168 | + "(both of SQL files on your machine)\n" 169 | + " java -jar createtable1.sql createtable2.sql\n" 170 | + "* Take diff between dbname1 and dbname2 " 171 | + "(both of databases on the local MySQL)\n" 172 | + " java -jar dbname1 dbname2\n" 173 | + "* Take diff between dbname1 and dbname2 " 174 | + "(both of databases on remote MySQL)\n" 175 | + " java -jar '-u root -h localhost dbname1' '-u root -h localhost dbname2'" 176 | + "\n" 177 | + "[Options]\n" 178 | + " -h, --help: Show usage\n" 179 | + " -v, --version: Show version"; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/DiffExtractor.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import net.moznion.mysql.diff.model.Column; 4 | import net.moznion.mysql.diff.model.OrdinaryKey; 5 | import net.moznion.mysql.diff.model.Table; 6 | import net.moznion.mysql.diff.model.UniqueKey; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.Map.Entry; 14 | import java.util.Set; 15 | import java.util.stream.Collectors; 16 | 17 | /** 18 | * Diff extractor for table definition of schema. 19 | * 20 | * @author moznion 21 | * 22 | */ 23 | public class DiffExtractor { 24 | /** 25 | * Extract diff between two schemas. 26 | * 27 | * @param oldTables tables of old schema. 28 | * @param newTables tables of new schema. 29 | * @return Diff string. 30 | */ 31 | public static String extractDiff(List
oldTables, List
newTables) { 32 | StringBuilder diffStringBuilder = new StringBuilder(); 33 | 34 | List newTableNames = newTables.stream() 35 | .map(table -> table.getTableName()) 36 | .sorted() 37 | .collect(Collectors.toList()); 38 | 39 | Map oldTableMap = oldTables.stream() 40 | .collect(Collectors.toMap(Table::getTableName, t -> t)); 41 | Map newTableMap = newTables.stream() 42 | .collect(Collectors.toMap(Table::getTableName, t -> t)); 43 | 44 | for (String tableName : newTableNames) { 45 | Table newTable = newTableMap.get(tableName); 46 | if (oldTableMap.containsKey(tableName)) { 47 | Table oldTable = oldTableMap.get(tableName); 48 | diffStringBuilder.append(extractTableDiff(tableName, oldTable, newTable)); 49 | } else { 50 | diffStringBuilder.append(newTable.getContent()).append(";\n\n"); 51 | } 52 | } 53 | 54 | return diffStringBuilder.toString(); 55 | } 56 | 57 | private static String extractTableDiff(String tableName, Table oldTable, Table newTable) { 58 | List changes = extractColumnDiff(oldTable, newTable); 59 | changes.addAll(extractKeyDiff(oldTable, newTable)); 60 | 61 | if (changes.isEmpty()) { 62 | return ""; 63 | } 64 | 65 | return new StringBuilder() 66 | .append("ALTER TABLE `") 67 | .append(tableName) 68 | .append("` ") 69 | .append(String.join(", ", changes)) 70 | .append(";\n\n") 71 | .toString(); 72 | } 73 | 74 | private static List extractColumnDiff(Table oldTable, Table newTable) { 75 | List oldColumns = oldTable.getColumns(); 76 | List newColumns = newTable.getColumns(); 77 | 78 | Map oldColumnMap = oldColumns.stream() 79 | .collect(Collectors.toMap(Column::getName, c -> c)); 80 | Map newColumnMap = newColumns.stream() 81 | .collect(Collectors.toMap(Column::getName, c -> c)); 82 | 83 | Map allColumnMap = new HashMap<>(); 84 | allColumnMap.putAll(oldColumnMap); 85 | allColumnMap.putAll(newColumnMap); 86 | 87 | List changes = new ArrayList<>(); 88 | for (Entry column : allColumnMap.entrySet()) { 89 | String columnName = column.getKey(); 90 | 91 | if (!oldColumnMap.containsKey(columnName)) { 92 | changes.add(new StringBuilder() 93 | .append("ADD `") 94 | .append(columnName) 95 | .append("` ") 96 | .append(newColumnMap.get(columnName).getDefinition()) 97 | .toString()); 98 | continue; 99 | } 100 | 101 | if (!newColumnMap.containsKey(columnName)) { 102 | changes.add(new StringBuilder() 103 | .append("DROP `") 104 | .append(columnName) 105 | .append("`") 106 | .toString()); 107 | continue; 108 | } 109 | 110 | String oldDefinition = oldColumnMap.get(columnName).getDefinition(); 111 | String newDefinition = newColumnMap.get(columnName).getDefinition(); 112 | if (!oldDefinition.equals(newDefinition)) { 113 | changes.add(new StringBuilder() 114 | .append("MODIFY `") 115 | .append(columnName) 116 | .append("` ") 117 | .append(newDefinition) 118 | .toString()); 119 | continue; 120 | } 121 | } 122 | 123 | return changes; 124 | } 125 | 126 | private static List extractKeyDiff(Table oldTable, Table newTable) { 127 | List changes = new ArrayList<>(); 128 | 129 | // For ordinary key 130 | changes.addAll(extractOrdinaryKeyDiff(oldTable, newTable)); 131 | 132 | // For unique key 133 | changes.addAll(extractUniqueKeyDiff(oldTable, newTable)); 134 | 135 | return changes; 136 | } 137 | 138 | private static List extractOrdinaryKeyDiff(Table oldTable, Table newTable) { 139 | List changes = new ArrayList<>(); 140 | 141 | List oldKeys = oldTable.getKeys(); 142 | List newKeys = newTable.getKeys(); 143 | 144 | Set oldKeysAttendance = oldKeys.stream() 145 | .map(OrdinaryKey::getColumn) 146 | .collect(Collectors.toSet()); 147 | Set newKeysAttendance = newKeys.stream() 148 | .map(OrdinaryKey::getColumn) 149 | .collect(Collectors.toSet()); 150 | 151 | // add key 152 | for (OrdinaryKey key : newKeys) { 153 | String column = key.getColumn(); 154 | if (oldKeysAttendance.contains(column)) { 155 | continue; 156 | } 157 | 158 | String name = String.join("_", 159 | Arrays.stream(column.split(",")) 160 | .map(col -> col.replaceAll("[`()]", "")) 161 | .collect(Collectors.toList())); 162 | 163 | changes.add( 164 | new StringBuilder() 165 | .append("ADD INDEX `") 166 | .append(name) 167 | .append("` (") 168 | .append(column) 169 | .append(")") 170 | .toString()); 171 | } 172 | 173 | // drop key 174 | for (OrdinaryKey key : oldKeys) { 175 | String column = key.getColumn(); 176 | if (newKeysAttendance.contains(column)) { 177 | continue; 178 | } 179 | 180 | changes.add( 181 | new StringBuilder() 182 | .append("DROP INDEX `") 183 | .append(key.getName()) 184 | .append("`") 185 | .toString()); 186 | } 187 | 188 | return changes; 189 | } 190 | 191 | private static List extractUniqueKeyDiff(Table oldTable, Table newTable) { 192 | List changes = new ArrayList<>(); 193 | 194 | List oldKeys = oldTable.getUniqueKeys(); 195 | List newKeys = newTable.getUniqueKeys(); 196 | 197 | Set oldKeysAtendance = oldKeys.stream() 198 | .map(OrdinaryKey::getColumn) 199 | .collect(Collectors.toSet()); 200 | Set newKeysAtendance = newKeys.stream() 201 | .map(OrdinaryKey::getColumn) 202 | .collect(Collectors.toSet()); 203 | 204 | // add key 205 | for (UniqueKey key : newKeys) { 206 | String column = key.getColumn(); 207 | if (oldKeysAtendance.contains(column)) { 208 | continue; 209 | } 210 | 211 | String name = String.join("_", 212 | Arrays.asList(column.split(",")).stream() 213 | .map(col -> col.replaceAll("[`()]", "")) 214 | .collect(Collectors.toList())); 215 | 216 | changes.add( 217 | new StringBuilder() 218 | .append("ADD UNIQUE INDEX `") 219 | .append(name) 220 | .append("` (") 221 | .append(column) 222 | .append(")") 223 | .toString()); 224 | } 225 | 226 | // drop key 227 | for (OrdinaryKey key : oldKeys) { 228 | String column = key.getColumn(); 229 | if (newKeysAtendance.contains(column)) { 230 | continue; 231 | } 232 | 233 | changes.add( 234 | new StringBuilder() 235 | .append("DROP INDEX `") 236 | .append(key.getName()) 237 | .append("`") 238 | .toString()); 239 | } 240 | 241 | return changes; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/MySqlConnectionInfo.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.net.URISyntaxException; 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | /** 13 | * Representation of connection information for MySQL. 14 | * 15 | * @author moznion 16 | * 17 | */ 18 | @Getter 19 | public class MySqlConnectionInfo { 20 | private final String host; 21 | private final String user; 22 | private final String pass; 23 | private final String jdbcUrl; 24 | 25 | /** 26 | * Builder class of MySqlConnectionInfo. 27 | * 28 | *

29 | * This class provides following setters; 30 | *

31 | *
    32 | *
  • host(String hostName) // default value: "localhost"
  • 33 | *
  • port(int portNumber) // default value: 3306
  • 34 | *
  • user(String userName) // default value: "root"
  • 35 | *
  • host(String password) // default value: ""
  • 36 | *
37 | */ 38 | @Accessors(fluent = true) 39 | public static class Builder { 40 | @Setter 41 | private String host = "localhost"; 42 | @Setter 43 | private int port = 3306; 44 | @Setter 45 | private String user = "root"; 46 | @Setter 47 | private String pass = ""; 48 | 49 | private List properties = new ArrayList<>(Arrays.asList("allowMultiQueries=true")); 50 | 51 | /** 52 | * Add a property. 53 | * 54 | * @param property 55 | */ 56 | public void addProperty(String property) { 57 | properties.add(property); 58 | } 59 | 60 | /** 61 | * Add properties. 62 | * 63 | * @param properties 64 | */ 65 | public void addProperties(List properties) { 66 | this.properties.addAll(properties); 67 | } 68 | 69 | /** 70 | * Set host name, port number and options by URL of a remote MySQL. 71 | * 72 | * @param url URL of a remote MySQL (e.g. 73 | * jdbc:mysql://localhost:8888/something_table?cacheServerConfiguration=true) 74 | * @throws URISyntaxException 75 | */ 76 | public Builder url(String url) throws URISyntaxException { 77 | MySqlConnectionUri connectionUri = new MySqlConnectionUri(url); 78 | 79 | host = connectionUri.getHost(); 80 | port = connectionUri.getPort(); 81 | addProperties(connectionUri.getQueries()); 82 | 83 | return this; 84 | } 85 | 86 | /** 87 | * Builds MySqlConnectionInfo. 88 | * 89 | * @return New MySqlConnectionInfo instance. 90 | */ 91 | public MySqlConnectionInfo build() { 92 | return new MySqlConnectionInfo(this); 93 | } 94 | } 95 | 96 | /** 97 | * Dispenses a new builder of MySqlConnectionInfo. 98 | * 99 | * @return Builder of MySqlConnectionInfo. 100 | */ 101 | public static Builder builder() { 102 | return new Builder(); 103 | } 104 | 105 | private MySqlConnectionInfo(Builder builder) { 106 | host = builder.host; 107 | user = builder.user; 108 | pass = builder.pass; 109 | jdbcUrl = new StringBuilder() 110 | .append("jdbc:mysql://") 111 | .append(builder.host) 112 | .append(":") 113 | .append(builder.port) 114 | .append("?") 115 | .append(String.join("&", builder.properties)) 116 | .toString(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/MySqlConnectionUri.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import lombok.Getter; 4 | 5 | import java.net.URI; 6 | import java.net.URISyntaxException; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | @Getter 14 | class MySqlConnectionUri { 15 | private String host; 16 | private int port; 17 | private List queries; 18 | 19 | private static final int DEFAULT_PORT = 3306; 20 | private static final Pattern URL_PATTERN = Pattern.compile("^([^/]*:)[^:/]+:"); 21 | 22 | public MySqlConnectionUri(String url) throws URISyntaxException { 23 | Matcher urlMatcher = URL_PATTERN.matcher(url); 24 | if (!urlMatcher.find()) { 25 | throw new URISyntaxException(url, "It doesn't contain connection protocol schema of jdbc"); 26 | } 27 | 28 | String cleanUrl = url.substring(urlMatcher.group(1).length()); 29 | URI uri = new URI(cleanUrl); 30 | 31 | host = uri.getHost(); 32 | 33 | port = uri.getPort(); 34 | if (port < 0) { 35 | port = DEFAULT_PORT; 36 | } 37 | 38 | queries = parseQueryString(uri.getQuery()); 39 | } 40 | 41 | private List parseQueryString(String queryString) { 42 | ArrayList queries = new ArrayList<>(); 43 | 44 | if (queryString != null) { 45 | Arrays.asList(queryString.split("&")).forEach(property -> { 46 | queries.add(property); 47 | }); 48 | } 49 | 50 | return queries; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/SchemaDumper.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import java.io.BufferedReader; 4 | import java.io.File; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.IOException; 8 | import java.nio.charset.Charset; 9 | import java.nio.charset.StandardCharsets; 10 | import java.nio.file.Files; 11 | import java.nio.file.Paths; 12 | import java.sql.Connection; 13 | import java.sql.DriverManager; 14 | import java.sql.SQLException; 15 | import java.sql.Statement; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.UUID; 20 | 21 | /** 22 | * Dumper for SQL table definition. 23 | * 24 | * @author moznion 25 | * 26 | */ 27 | public class SchemaDumper { 28 | private static final String LINE_SEPARATOR = System.lineSeparator(); 29 | 30 | private final MySqlConnectionInfo localMySqlConnectionInfo; 31 | private final String mysqldumpPath; 32 | 33 | /** 34 | * Instantiate SchemaDumper. 35 | * 36 | * @param localMySqlConnectionInfo Connection information of MySQL which is on your local 37 | * environment. 38 | * @param mysqldumpPath Path for mysqldump command. 39 | */ 40 | public SchemaDumper(MySqlConnectionInfo localMySqlConnectionInfo, String mysqldumpPath) { 41 | if (localMySqlConnectionInfo == null) { 42 | throw new IllegalArgumentException("mysqlConnectionInfo must not be null"); 43 | } 44 | 45 | if (mysqldumpPath == null) { 46 | throw new IllegalArgumentException("mysqldumpPath must not be null"); 47 | } 48 | 49 | this.localMySqlConnectionInfo = localMySqlConnectionInfo; 50 | this.mysqldumpPath = mysqldumpPath; 51 | } 52 | 53 | /** 54 | * Instantiate SchemaDumper. 55 | * 56 | *

57 | * Path of mysqldump will be used default as "mysqldump". 58 | *

59 | * 60 | * @param localMySqlConnectionInfo Connection information of MySQL which is on your local 61 | * environment. 62 | */ 63 | public SchemaDumper(MySqlConnectionInfo localMySqlConnectionInfo) { 64 | this(localMySqlConnectionInfo, "mysqldump"); 65 | } 66 | 67 | /** 68 | * Instantiate SchemaDumper. 69 | * 70 | *

71 | * Connection information of local MySQL will be used default as "-h localhost -u root". 72 | *

73 | * 74 | * @param mysqldumpPath Path for mysqldump command. 75 | */ 76 | public SchemaDumper(String mysqldumpPath) { 77 | this(MySqlConnectionInfo.builder().build(), mysqldumpPath); 78 | } 79 | 80 | /** 81 | * Instantiate SchemaDumper. 82 | * 83 | *

84 | * Path of mysqldump will be used default as "mysqldump".
85 | * Connection information of local MySQL will be used default as "-h localhost -u root". 86 | *

87 | */ 88 | public SchemaDumper() { 89 | this(MySqlConnectionInfo.builder().build()); 90 | } 91 | 92 | /** 93 | * Dump schema from SQL string. 94 | * 95 | * @param sql SQL string which is a target to dump. 96 | * @return Result of dumping. 97 | * @throws SQLException Throw if invalid SQL is given. 98 | * @throws IOException Throw if mysqldump command is failed. 99 | * @throws InterruptedException Throw if mysqldump command is failed. 100 | */ 101 | public String dump(String sql) throws SQLException, IOException, InterruptedException { 102 | String tempDbName = new StringBuilder() 103 | .append("tmp_") 104 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 105 | .toString(); 106 | 107 | String mysqlUrl = localMySqlConnectionInfo.getJdbcUrl(); 108 | String mysqlUser = localMySqlConnectionInfo.getUser(); 109 | String mysqlPass = localMySqlConnectionInfo.getPass(); 110 | try (Connection connection = DriverManager.getConnection(mysqlUrl, mysqlUser, mysqlPass)) { 111 | try (Statement stmt = connection.createStatement()) { 112 | stmt.executeUpdate("CREATE DATABASE " + tempDbName); 113 | } 114 | 115 | try (Statement stmt = connection.createStatement()) { 116 | stmt.execute(new StringBuilder() 117 | .append("USE ") 118 | .append(tempDbName) 119 | .append("; ") 120 | .append(sql) 121 | .toString()); 122 | } 123 | 124 | return fetchSchemaViaMysqldump(tempDbName); 125 | } catch (Exception e) { 126 | throw e; 127 | } finally { 128 | try (Connection connection = DriverManager.getConnection(mysqlUrl, mysqlUser, mysqlPass)) { 129 | try (Statement stmt = connection.createStatement()) { 130 | stmt.executeUpdate("DROP DATABASE " + tempDbName); 131 | } 132 | } 133 | } 134 | } 135 | 136 | /** 137 | * Dump schema from SQL file. 138 | * 139 | * @param sqlFile SQL file. 140 | * @param charset Character set of SQL file. 141 | * @return Result of dumping. 142 | * @throws SQLException Throw if invalid SQL is given. 143 | * @throws IOException Throw if mysqldump command is failed. 144 | * @throws InterruptedException Throw if mysqldump command is failed. 145 | */ 146 | public String dump(File sqlFile, Charset charset) 147 | throws IOException, SQLException, InterruptedException { 148 | String sqlString = 149 | new String(Files.readAllBytes(Paths.get(sqlFile.getAbsolutePath())), charset); 150 | return dump(sqlString); 151 | } 152 | 153 | /** 154 | * Dump schema from SQL file which is written by UTF-8. 155 | * 156 | * @param sqlFile SQL file (written by UTF-8). 157 | * @return Result of dumping. 158 | * @throws SQLException Throw if invalid SQL is given. 159 | * @throws IOException Throw if mysqldump command is failed. 160 | * @throws InterruptedException Throw if mysqldump command is failed. 161 | */ 162 | public String dump(File sqlFile) throws IOException, SQLException, InterruptedException { 163 | return dump(sqlFile, StandardCharsets.UTF_8); 164 | } 165 | 166 | /** 167 | * Dump schema from DB name which is in local MySQL. 168 | * 169 | * @param dbName DB name which is in local MySQL. 170 | * @return Result of dumping. 171 | * @throws SQLException Throw if invalid SQL is given. 172 | * @throws IOException Throw if mysqldump command is failed. 173 | * @throws InterruptedException Throw if mysqldump command is failed. 174 | */ 175 | public String dumpFromLocalDb(String dbName) 176 | throws IOException, InterruptedException, SQLException { 177 | return fetchSchemaViaMysqldump(dbName); 178 | } 179 | 180 | /** 181 | * Dump schema from DB name which is in remote MySQL. 182 | * 183 | * @param dbName DB name which is in remote MySQL. 184 | * @param mysqlConnectionInfo Connection information of remote MySQL. 185 | * @return Result of dumping. 186 | * @throws SQLException Throw if invalid SQL is given. 187 | * @throws IOException Throw if mysqldump command is failed. 188 | * @throws InterruptedException Throw if mysqldump command is failed. 189 | */ 190 | public String dumpFromRemoteDb(String dbName, MySqlConnectionInfo mysqlConnectionInfo) 191 | throws IOException, InterruptedException, SQLException { 192 | String schema = fetchSchemaViaMysqldump(dbName, mysqlConnectionInfo); 193 | return dump(schema); 194 | } 195 | 196 | private String fetchSchemaViaMysqldump(String dbName, MySqlConnectionInfo mysqlConnectionInfo) 197 | throws IOException, InterruptedException { 198 | String schema; 199 | List mysqldumpCommand = new ArrayList<>(Arrays.asList( 200 | mysqldumpPath, 201 | "--no-data=true", 202 | dbName)); 203 | 204 | String mysqlUser = mysqlConnectionInfo.getUser(); 205 | if (!mysqlUser.isEmpty()) { 206 | mysqldumpCommand.add(new StringBuilder().append("-u").append(mysqlUser).toString()); 207 | } 208 | 209 | String mysqlHost = mysqlConnectionInfo.getHost(); 210 | if (!mysqlHost.isEmpty()) { 211 | mysqldumpCommand.add(new StringBuilder().append("-h").append(mysqlHost).toString()); 212 | } 213 | 214 | ProcessBuilder processBuilder = new ProcessBuilder(mysqldumpCommand); 215 | 216 | Process process = processBuilder.start(); 217 | try (InputStream inputStream = process.getInputStream()) { 218 | StringBuilder stdoutStringBuilder = new StringBuilder(); 219 | try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { 220 | String line; 221 | while ((line = bufferedReader.readLine()) != null) { 222 | stdoutStringBuilder.append(line).append(LINE_SEPARATOR); 223 | } 224 | } 225 | schema = stdoutStringBuilder.toString(); 226 | } 227 | 228 | if (process.waitFor() != 0) { 229 | throw new RuntimeException( 230 | new StringBuilder() 231 | .append("Failed to execute `mysqldump` command.\n") 232 | .append("[command] >>>\n") 233 | .append(String.join(" ", mysqldumpCommand)) 234 | .append("\n<<<\n") 235 | .append("[output] >>>\n") 236 | .append(schema) 237 | .append("\n<<<\n") 238 | .toString()); 239 | } 240 | 241 | return schema; 242 | } 243 | 244 | private String fetchSchemaViaMysqldump(String dbName) 245 | throws IOException, InterruptedException, SQLException { 246 | return fetchSchemaViaMysqldump(dbName, localMySqlConnectionInfo); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/SchemaParser.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import net.moznion.mysql.diff.model.Column; 4 | import net.moznion.mysql.diff.model.OrdinaryKey; 5 | import net.moznion.mysql.diff.model.Table; 6 | import net.moznion.mysql.diff.model.UniqueKey; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | 13 | /** 14 | * Dumper for SQL table definition. 15 | * 16 | * @author moznion 17 | * 18 | */ 19 | public class SchemaParser { 20 | private static final Pattern TABLA_BLOCK_PATTERN = Pattern.compile( 21 | "CREATE TABLE .*? ENGINE[^;]*", Pattern.MULTILINE | Pattern.DOTALL); 22 | 23 | private static final Pattern TABLE_NAME_PATTERN = Pattern.compile("`(.*?)`"); 24 | 25 | private static final Pattern PRIMARY_KEY_PATTERN = Pattern.compile( 26 | "^\\s*PRIMARY KEY\\s+\\((.*)\\)"); 27 | 28 | private static final Pattern UNIQUE_KEY_PATTERN = Pattern.compile( 29 | "^\\s*UNIQUE KEY\\s+`(.*)`\\s+\\((.*)\\)"); 30 | 31 | private static final Pattern ORDINARY_KEY_PATTERN = Pattern.compile( 32 | "^\\s*KEY\\s+`(.*)`\\s+\\((.*)\\)"); 33 | 34 | private static final Pattern COLUMN_PATTERN = Pattern.compile( 35 | "^\\s*`(.*?)`\\s+(.+?)[\n,]?$"); 36 | 37 | /** 38 | * Parse and output table definition of given schema. 39 | * 40 | * @param schema Schema which is generated by SchemaDumper. 41 | * @return Table definition of given schema. 42 | */ 43 | public static List
parse(String schema) { 44 | Matcher blockMatcher = TABLA_BLOCK_PATTERN.matcher(schema); 45 | 46 | List
tables = new ArrayList<>(); 47 | 48 | while (blockMatcher.find()) { 49 | String content = blockMatcher.group(); 50 | 51 | Matcher tableNameMatcher = TABLE_NAME_PATTERN.matcher(content); 52 | if (!tableNameMatcher.find()) { 53 | continue; 54 | } 55 | String tableName = tableNameMatcher.group(1); 56 | 57 | List primaryKeys = new ArrayList<>(); 58 | List uniqueKeys = new ArrayList<>(); 59 | List keys = new ArrayList<>(); 60 | List columns = new ArrayList<>(); 61 | 62 | for (String line : content.split("\r?\n")) { 63 | if (line.matches("^CREATE") || line.matches("^\\)")) { 64 | continue; 65 | } 66 | 67 | Matcher primaryKeyMatcher = PRIMARY_KEY_PATTERN.matcher(line); 68 | if (primaryKeyMatcher.find()) { 69 | primaryKeys.add(primaryKeyMatcher.group(1)); 70 | continue; 71 | } 72 | 73 | Matcher uniqueKeyMatcher = UNIQUE_KEY_PATTERN.matcher(line); 74 | if (uniqueKeyMatcher.find()) { 75 | uniqueKeys.add(new UniqueKey(uniqueKeyMatcher.group(1), uniqueKeyMatcher.group(2))); 76 | continue; 77 | } 78 | 79 | Matcher ordinaryKeyMatcher = ORDINARY_KEY_PATTERN.matcher(line); 80 | if (ordinaryKeyMatcher.find()) { 81 | keys.add(new OrdinaryKey(ordinaryKeyMatcher.group(1), ordinaryKeyMatcher.group(2))); 82 | continue; 83 | } 84 | 85 | Matcher columnMatcher = COLUMN_PATTERN.matcher(line); 86 | if (columnMatcher.find()) { 87 | columns.add(new Column(columnMatcher.group(1), columnMatcher.group(2))); 88 | continue; 89 | } 90 | 91 | // Match nothing if reach here 92 | } 93 | 94 | tables.add(Table.builder() 95 | .tableName(tableName) 96 | .primaryKeys(primaryKeys) 97 | .keys(keys) 98 | .uniqueKeys(uniqueKeys) 99 | .columns(columns) 100 | .content(content) 101 | .build()); 102 | } 103 | 104 | return tables; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/model/Column.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff.model; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class Column { 7 | private final String name; 8 | private final String definition; 9 | 10 | public Column(String name, String definition) { 11 | this.name = name; 12 | this.definition = definition; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/model/OrdinaryKey.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff.model; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class OrdinaryKey { 7 | private final String name; 8 | private final String column; 9 | 10 | public OrdinaryKey(String name, String column) { 11 | this.name = name; 12 | this.column = column; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/model/Table.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import lombok.experimental.Accessors; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | @Getter 12 | public class Table { 13 | private final String tableName; 14 | private final List primaryKeys; 15 | private final List uniqueKeys; 16 | private final List keys; 17 | private final List columns; 18 | private final String content; 19 | 20 | @Setter 21 | @Accessors(fluent = true) 22 | public static class Builder { 23 | private String tableName; 24 | private List primaryKeys; 25 | private List uniqueKeys; 26 | private List keys; 27 | private List columns; 28 | private String content; 29 | 30 | public Builder() {} 31 | 32 | public Table build() { 33 | return new Table(this); 34 | } 35 | } 36 | 37 | public static Builder builder() { 38 | return new Builder(); 39 | } 40 | 41 | private Table(Builder builder) { 42 | tableName = Optional.ofNullable(builder.tableName) 43 | .orElseThrow(() -> new IllegalArgumentException("Missing table name")); 44 | primaryKeys = Optional.ofNullable(builder.primaryKeys).orElse(new ArrayList<>()); 45 | uniqueKeys = Optional.ofNullable(builder.uniqueKeys).orElse(new ArrayList<>()); 46 | keys = Optional.ofNullable(builder.keys).orElse(new ArrayList<>()); 47 | columns = Optional.ofNullable(builder.columns).orElse(new ArrayList<>()); 48 | content = Optional.ofNullable(builder.content).orElse(""); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/net/moznion/mysql/diff/model/UniqueKey.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff.model; 2 | 3 | public class UniqueKey extends OrdinaryKey { 4 | public UniqueKey(String name, String column) { 5 | super(name, column); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/net/moznion/mysql/diff/AppTest.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import com.mysql.cj.jdbc.exceptions.CommunicationsException; 4 | import java.io.BufferedWriter; 5 | import java.io.ByteArrayOutputStream; 6 | import java.io.File; 7 | import java.io.FileDescriptor; 8 | import java.io.FileOutputStream; 9 | import java.io.IOException; 10 | import java.io.OutputStreamWriter; 11 | import java.io.PrintStream; 12 | import java.nio.charset.Charset; 13 | import java.sql.Connection; 14 | import java.sql.DriverManager; 15 | import java.sql.SQLException; 16 | import java.sql.Statement; 17 | import java.util.Arrays; 18 | import java.util.UUID; 19 | import org.junit.Test; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertTrue; 23 | import static org.junit.Assume.assumeTrue; 24 | 25 | public class AppTest { 26 | private static final String SQL_FOR_TEST = "CREATE TABLE `sample` (\n" 27 | + " `id` int(10) NOT NULL AUTO_INCREMENT,\n" 28 | + " PRIMARY KEY (`id`)\n" 29 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;\n"; 30 | 31 | @Test 32 | public void shouldShowVersion() throws IOException, SQLException, InterruptedException { 33 | // For short option 34 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 35 | System.setOut(new PrintStream(baos)); 36 | 37 | String[] argsForShortOpt = {"-v"}; 38 | App.main(argsForShortOpt); 39 | 40 | String versionString = baos.toString(); 41 | System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); 42 | assertEquals("Missing Version\n", versionString); 43 | 44 | // For long option 45 | baos = new ByteArrayOutputStream(); 46 | System.setOut(new PrintStream(baos)); 47 | 48 | String[] argsForLongOpt = {"--version"}; 49 | App.main(argsForLongOpt); 50 | 51 | versionString = baos.toString(); 52 | System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); 53 | assertEquals("Missing Version\n", versionString); 54 | } 55 | 56 | @Test 57 | public void shouldShowUsageByOption() throws IOException, SQLException, InterruptedException { 58 | // For short option 59 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 60 | System.setOut(new PrintStream(baos)); 61 | 62 | String[] argsForShortOpt = {"-h"}; 63 | App.main(argsForShortOpt); 64 | 65 | String usageString = baos.toString(); 66 | 67 | String expectedUsageString = "[Usage]\n" 68 | + " java -jar [old_database] [new_database]\n" 69 | + "[Examples]\n" 70 | + "* Take diff between createtable1.sql and createtable2.sql " 71 | + "(both of SQL files on your machine)\n" 72 | + " java -jar createtable1.sql createtable2.sql\n" 73 | + "* Take diff between dbname1 and dbname2 " 74 | + "(both of databases on the local MySQL)\n" 75 | + " java -jar dbname1 dbname2\n" 76 | + "* Take diff between dbname1 and dbname2 " 77 | + "(both of databases on remote MySQL)\n" 78 | + " java -jar '-u root -h localhost dbname1' '-u root -h localhost dbname2'" 79 | + "\n" 80 | + "[Options]\n" 81 | + " -h, --help: Show usage\n" 82 | + " -v, --version: Show version\n"; 83 | 84 | System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); 85 | assertEquals(expectedUsageString, usageString); 86 | 87 | // For long option 88 | baos = new ByteArrayOutputStream(); 89 | System.setOut(new PrintStream(baos)); 90 | 91 | String[] argsForLongOpt = {"--help"}; 92 | App.main(argsForLongOpt); 93 | 94 | usageString = baos.toString(); 95 | 96 | System.setOut(new PrintStream(new FileOutputStream(FileDescriptor.out))); 97 | assertEquals(expectedUsageString, usageString); 98 | } 99 | 100 | @Test 101 | public void shouldTakeDiffBetweenFiles() throws IOException, SQLException, InterruptedException { 102 | File sqlFile1 = File.createTempFile("tempsql1", ".sql"); 103 | File sqlFile2 = File.createTempFile("tempsql2", ".sql"); 104 | 105 | for (File sqlFile : Arrays.asList(sqlFile1, sqlFile2)) { 106 | try (BufferedWriter bufferedWriter = 107 | new BufferedWriter(new OutputStreamWriter(new FileOutputStream(sqlFile), 108 | Charset.forName("UTF-8")))) { 109 | bufferedWriter.write(SQL_FOR_TEST); 110 | } 111 | } 112 | 113 | String[] args = {sqlFile1.getAbsolutePath(), sqlFile2.getAbsolutePath()}; 114 | try { 115 | App.main(args); 116 | } catch (CommunicationsException e) { 117 | assumeTrue("MySQL maybe not launched", false); 118 | } 119 | 120 | assertTrue(true); 121 | 122 | sqlFile1.delete(); 123 | sqlFile2.delete(); 124 | } 125 | 126 | @Test 127 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( 128 | value = "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE") 129 | public void shouldTakeDiffBetweenLocalDb() 130 | throws IOException, SQLException, InterruptedException { 131 | MySqlConnectionInfo connInfo = MySqlConnectionInfo.builder().build(); 132 | 133 | String tempDbName1 = new StringBuilder() 134 | .append("tmp_") 135 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 136 | .toString(); 137 | String tempDbName2 = new StringBuilder() 138 | .append("tmp_") 139 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 140 | .toString(); 141 | 142 | String mysqlUrl = connInfo.getJdbcUrl(); 143 | String user = connInfo.getUser(); 144 | String pass = connInfo.getPass(); 145 | 146 | try (Connection connection = DriverManager.getConnection(mysqlUrl, user, pass)) { 147 | for (String dbName : Arrays.asList(tempDbName1, tempDbName2)) { 148 | try (Statement stmt = connection.createStatement()) { 149 | stmt.executeUpdate("CREATE DATABASE " + dbName); 150 | } 151 | try (Statement stmt = connection.createStatement()) { 152 | stmt.executeUpdate("USE " + dbName + "; " + SQL_FOR_TEST); 153 | } 154 | } 155 | 156 | String[] args = {tempDbName1, tempDbName2}; 157 | App.main(args); 158 | } catch (CommunicationsException e) { 159 | assumeTrue("MySQL maybe not launched", false); 160 | return; 161 | } catch (Exception e) { 162 | assertTrue(false); 163 | throw e; 164 | } finally { 165 | try (Connection connectionToTeardown = DriverManager.getConnection(mysqlUrl, user, pass)) { 166 | for (String dbName : Arrays.asList(tempDbName1, tempDbName2)) { 167 | try (Statement stmt = connectionToTeardown.createStatement()) { 168 | stmt.executeUpdate("DROP DATABASE " + dbName); 169 | } 170 | } 171 | } catch (CommunicationsException e) { 172 | assumeTrue("MySQL maybe not launched", false); 173 | } 174 | } 175 | 176 | assertTrue(true); 177 | } 178 | 179 | @Test 180 | @edu.umd.cs.findbugs.annotations.SuppressFBWarnings( 181 | value = "SQL_NONCONSTANT_STRING_PASSED_TO_EXECUTE") 182 | public void shouldTakeDiffBetweenRemoteDb() throws IOException, SQLException, 183 | InterruptedException { 184 | MySqlConnectionInfo connInfo = MySqlConnectionInfo.builder().build(); 185 | 186 | final String tempDbName1 = new StringBuilder() 187 | .append("tmp_") 188 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 189 | .toString(); 190 | final String tempDbName2 = new StringBuilder() 191 | .append("tmp_") 192 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 193 | .toString(); 194 | 195 | String mysqlUrl = connInfo.getJdbcUrl(); 196 | String user = connInfo.getUser(); 197 | String pass = connInfo.getPass(); 198 | 199 | try (Connection connection = DriverManager.getConnection(mysqlUrl, user, pass)) { 200 | for (String dbName : Arrays.asList(tempDbName1, tempDbName2)) { 201 | try (Statement stmt = connection.createStatement()) { 202 | stmt.executeUpdate("CREATE DATABASE " + dbName); 203 | } 204 | try (Statement stmt = connection.createStatement()) { 205 | stmt.executeUpdate("USE " + dbName + "; " + SQL_FOR_TEST); 206 | } 207 | } 208 | 209 | String[] args = { 210 | "'-u root -h localhost " + tempDbName1 + "'", 211 | "'-u root -h localhost " + tempDbName2 + "'" 212 | }; 213 | App.main(args); 214 | } catch (CommunicationsException e) { 215 | assumeTrue("MySQL maybe not launched", false); 216 | return; 217 | } catch (Exception e) { 218 | throw e; 219 | } finally { 220 | try (Connection connectionToTeardown = DriverManager.getConnection(mysqlUrl, user, pass)) { 221 | for (String dbName : Arrays.asList(tempDbName1, tempDbName2)) { 222 | try (Statement stmt = connectionToTeardown.createStatement()) { 223 | stmt.executeUpdate("DROP DATABASE " + dbName); 224 | } 225 | } 226 | } catch (CommunicationsException e) { 227 | assumeTrue("MySQL maybe not launched", false); 228 | } 229 | } 230 | 231 | assertTrue(true); 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/test/java/net/moznion/mysql/diff/ConnectionInfoTest.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import static org.junit.Assert.assertTrue; 4 | 5 | import org.junit.Test; 6 | 7 | import java.net.URISyntaxException; 8 | 9 | public class ConnectionInfoTest { 10 | @Test 11 | public void shouldConnectSuccessfullyThroughFullyUrl() { 12 | try { 13 | new SchemaDumper( 14 | MySqlConnectionInfo 15 | .builder() 16 | .url( 17 | "jdbc:log4j:mysql://localhost:3306/something_table?" 18 | + "cacheServerConfiguration=true&useLocalSessionState=true") 19 | .build()); 20 | assertTrue(true); 21 | } catch (Exception e) { 22 | assertTrue(false); 23 | } 24 | } 25 | 26 | @Test 27 | public void shouldConnectSuccessfullyThroughUrlWithoutPort() { 28 | try { 29 | new SchemaDumper( 30 | MySqlConnectionInfo 31 | .builder() 32 | .url( 33 | "jdbc:log4j:mysql://localhost/something_table?" 34 | + "cacheServerConfiguration=true&useLocalSessionState=true") 35 | .build()); 36 | assertTrue(true); 37 | } catch (Exception e) { 38 | assertTrue(false); 39 | } 40 | } 41 | 42 | @Test 43 | public void shouldConnectSuccessfullyThroughUrlWithoutOptions() { 44 | try { 45 | new SchemaDumper(MySqlConnectionInfo.builder() 46 | .url("jdbc:log4j:mysql://localhost/something_table") 47 | .build()); 48 | assertTrue(true); 49 | } catch (Exception e) { 50 | assertTrue(false); 51 | } 52 | } 53 | 54 | @Test 55 | public void shouldConnectSuccessfullyThroughUrlWithoutConnectionPrefix() { 56 | try { 57 | new SchemaDumper( 58 | MySqlConnectionInfo 59 | .builder() 60 | .url("localhost/something_table") 61 | .build()); 62 | assertTrue(false); 63 | } catch (URISyntaxException e) { 64 | assertTrue(true); 65 | } catch (Exception e) { 66 | assertTrue(false); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/net/moznion/mysql/diff/DiffExtractorTest.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import com.mysql.cj.jdbc.exceptions.CommunicationsException; 4 | import java.io.IOException; 5 | import java.sql.SQLException; 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | import java.util.stream.Stream; 11 | import net.moznion.mysql.diff.model.Table; 12 | import org.junit.Test; 13 | import org.junit.experimental.runners.Enclosed; 14 | import org.junit.runner.RunWith; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertTrue; 18 | import static org.junit.Assume.assumeTrue; 19 | 20 | @RunWith(Enclosed.class) 21 | public class DiffExtractorTest { 22 | private static final SchemaDumper SCHEMA_DUMPER = new SchemaDumper(); 23 | 24 | public static class ForTableDiff { 25 | private String getAlterTableDiffRightly(String oldSql, String newSql) throws SQLException, 26 | IOException, InterruptedException { 27 | String oldSchema = SCHEMA_DUMPER.dump(oldSql); 28 | String newSchema = SCHEMA_DUMPER.dump(newSql); 29 | 30 | List
oldTables = SchemaParser.parse(oldSchema); 31 | List
newTables = SchemaParser.parse(newSchema); 32 | 33 | String diff = DiffExtractor.extractDiff(oldTables, newTables); 34 | diff = diff.replaceAll("\r?\n", ""); 35 | 36 | assertTrue(diff.startsWith("ALTER TABLE `sample` ")); 37 | assertTrue(diff.endsWith(";")); 38 | diff = diff.replaceFirst("^ALTER TABLE `sample` ", ""); 39 | diff = diff.replaceFirst(";$", ""); 40 | 41 | return diff; 42 | } 43 | 44 | @Test 45 | public void shouldDetectColumnDiffRightly() throws SQLException, IOException, 46 | InterruptedException { 47 | String oldSql = "CREATE TABLE `sample` (" 48 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 49 | + " `title` varchar(64) NOT NULL," 50 | + " `created_on` int(10) unsigned NOT NULL," 51 | + " PRIMARY KEY (`id`)" 52 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 53 | String newSql = "CREATE TABLE `sample` (" 54 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 55 | + " `title` varchar(64) DEFAULT NULL," 56 | + " `updated_on` int(10) unsigned NOT NULL," 57 | + " PRIMARY KEY (`id`)" 58 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 59 | 60 | try { 61 | String diff = getAlterTableDiffRightly(oldSql, newSql); 62 | Set modifiers = Arrays.stream(diff.split(", ")).collect(Collectors.toSet()); 63 | Set expected = Stream.of( 64 | "DROP `created_on`", 65 | "MODIFY `title` varchar(64) DEFAULT NULL", 66 | "ADD `updated_on` int(10) unsigned NOT NULL" 67 | ).collect(Collectors.toSet()); 68 | assertEquals(modifiers, expected); 69 | } catch (CommunicationsException e) { 70 | assumeTrue("MySQL maybe not launched", false); 71 | } catch (Exception e) { 72 | assertTrue(false); 73 | } 74 | } 75 | 76 | @Test 77 | public void shouldDetectKeyDiffRightly() 78 | throws SQLException, IOException, InterruptedException { 79 | String oldSql = "CREATE TABLE `sample` (" 80 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 81 | + " `name` varchar(16) NOT NULL," 82 | + " `email` varchar(16) NOT NULL," 83 | + " `title` varchar(64) NOT NULL," 84 | + " `created_on` int (10) unsigned NOT NULL," 85 | + " `updated_on` int (10) unsigned NOT NULL," 86 | + " PRIMARY KEY (`id`)," 87 | + " UNIQUE KEY `name` (`name`)," 88 | + " KEY `updated_on` (`updated_on`)" 89 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 90 | String newSql = "CREATE TABLE `sample` (" 91 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 92 | + " `name` varchar(16) NOT NULL," 93 | + " `email` varchar(16) NOT NULL," 94 | + " `title` varchar(64) NOT NULL," 95 | + " `created_on` int (10) unsigned NOT NULL," 96 | + " `updated_on` int (10) unsigned NOT NULL," 97 | + " PRIMARY KEY (`id`)," 98 | + " UNIQUE KEY `identifier` (`email`,`name`)," 99 | + " KEY `timestamp` (`created_on`,`updated_on`)" 100 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 101 | 102 | try { 103 | String diff = getAlterTableDiffRightly(oldSql, newSql); 104 | Set modifiers = Arrays.stream(diff.split(", ")).collect(Collectors.toSet()); 105 | Set expected = Stream.of( 106 | "ADD INDEX `created_on_updated_on` (`created_on`,`updated_on`)", 107 | "DROP INDEX `updated_on`", 108 | "ADD UNIQUE INDEX `email_name` (`email`,`name`)", 109 | "DROP INDEX `name`" 110 | ).collect(Collectors.toSet()); 111 | assertEquals(modifiers, expected); 112 | } catch (CommunicationsException e) { 113 | assumeTrue("MySQL maybe not launched", false); 114 | } catch (Exception e) { 115 | assertTrue(false); 116 | } 117 | } 118 | } 119 | 120 | public static class ForMissingTable { 121 | @Test 122 | public void shouldDetectMissingTableRightly() throws SQLException, IOException, 123 | InterruptedException { 124 | String oldSql = "CREATE TABLE `sample` (" 125 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 126 | + " PRIMARY KEY (`id`)" 127 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 128 | String newSql = "CREATE TABLE `new_one` (" 129 | + " `id` int(10) NOT NULL AUTO_INCREMENT," 130 | + " PRIMARY KEY (`id`)" 131 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; 132 | 133 | try { 134 | String oldSchema = SCHEMA_DUMPER.dump(oldSql); 135 | String newSchema = SCHEMA_DUMPER.dump(newSql); 136 | 137 | List
oldTables = SchemaParser.parse(oldSchema); 138 | List
newTables = SchemaParser.parse(newSchema); 139 | 140 | String diff = DiffExtractor.extractDiff(oldTables, newTables); 141 | List got = Arrays.stream(diff.split("\r?\n")) 142 | .map(line -> line.trim()) 143 | .collect(Collectors.toList()); 144 | List expected = Arrays.asList( 145 | "CREATE TABLE `new_one` (", 146 | "`id` int(10) NOT NULL AUTO_INCREMENT,", 147 | "PRIMARY KEY (`id`)", 148 | ") ENGINE=InnoDB DEFAULT CHARSET=utf8;"); 149 | assertEquals(got, expected); 150 | } catch (CommunicationsException e) { 151 | assumeTrue("MySQL maybe not launched", false); 152 | } catch (Exception e) { 153 | assertTrue(false); 154 | } 155 | } 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/net/moznion/mysql/diff/SchemaDumperTest.java: -------------------------------------------------------------------------------- 1 | package net.moznion.mysql.diff; 2 | 3 | import com.mysql.cj.jdbc.exceptions.CommunicationsException; 4 | import java.io.BufferedWriter; 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.OutputStreamWriter; 9 | import java.nio.charset.Charset; 10 | import java.sql.Connection; 11 | import java.sql.DriverManager; 12 | import java.sql.SQLException; 13 | import java.sql.Statement; 14 | import java.util.UUID; 15 | import org.junit.Test; 16 | import org.junit.experimental.runners.Enclosed; 17 | import org.junit.runner.RunWith; 18 | 19 | import static org.junit.Assert.assertTrue; 20 | import static org.junit.Assume.assumeTrue; 21 | 22 | @RunWith(Enclosed.class) 23 | public class SchemaDumperTest { 24 | private static final String SQL_FOR_TEST = "CREATE TABLE `sample` (\n" 25 | + " `id` int(10) NOT NULL AUTO_INCREMENT,\n" 26 | + " PRIMARY KEY (`id`)\n" 27 | + ") ENGINE=InnoDB DEFAULT CHARSET=utf8;\n"; 28 | 29 | public static class ForConstructors { 30 | @Test 31 | public void shouldInstantiateAsDefault() 32 | throws SQLException, IOException, InterruptedException { 33 | SchemaDumper schemaDumper = new SchemaDumper(); 34 | try { 35 | schemaDumper.dump(SQL_FOR_TEST); 36 | assertTrue(true); 37 | } catch (CommunicationsException e) { 38 | assumeTrue("MySQL maybe not launched", false); 39 | } catch (Exception e) { 40 | assertTrue(false); 41 | } 42 | } 43 | 44 | @Test 45 | public void shouldInstantiateByMysqldumpPath() throws SQLException, IOException, 46 | InterruptedException { 47 | SchemaDumper schemaDumper = new SchemaDumper("mysqldump"); 48 | try { 49 | schemaDumper.dump(SQL_FOR_TEST); 50 | assertTrue(true); 51 | } catch (CommunicationsException e) { 52 | assumeTrue("MySQL maybe not launched", false); 53 | } catch (Exception e) { 54 | assertTrue(false); 55 | } 56 | } 57 | 58 | @Test 59 | public void shouldInstantiateByLocalMySqlConnectionInfo() throws SQLException, IOException, 60 | InterruptedException { 61 | SchemaDumper schemaDumper = new SchemaDumper(MySqlConnectionInfo.builder().build()); 62 | try { 63 | schemaDumper.dump(SQL_FOR_TEST); 64 | assertTrue(true); 65 | } catch (CommunicationsException e) { 66 | assumeTrue("MySQL maybe not launched", false); 67 | } catch (Exception e) { 68 | assertTrue(false); 69 | } 70 | } 71 | 72 | @Test 73 | public void shouldInstantiateByAllArgs() 74 | throws SQLException, IOException, InterruptedException { 75 | SchemaDumper schemaDumper = 76 | new SchemaDumper(MySqlConnectionInfo.builder().build(), "mysqldump"); 77 | try { 78 | schemaDumper.dump(SQL_FOR_TEST); 79 | assertTrue(true); 80 | } catch (CommunicationsException e) { 81 | assumeTrue("MySQL maybe not launched", false); 82 | } catch (Exception e) { 83 | assertTrue(false); 84 | } 85 | } 86 | 87 | @Test 88 | public void shouldRaiseIllegalArgumentExceptionByNullConnectionInfo() { 89 | try { 90 | new SchemaDumper(null, "mysqldump"); 91 | } catch (IllegalArgumentException e) { 92 | assertTrue(true); 93 | return; 94 | } catch (Exception e) { 95 | assertTrue(false); 96 | } 97 | assertTrue(false); 98 | } 99 | 100 | @Test 101 | public void shouldRaiseIllegalArgumentExceptionByNullMysqldumpPath() { 102 | try { 103 | new SchemaDumper(MySqlConnectionInfo.builder().build(), null); 104 | } catch (IllegalArgumentException e) { 105 | assertTrue(true); 106 | return; 107 | } catch (Exception e) { 108 | assertTrue(false); 109 | } 110 | assertTrue(false); 111 | } 112 | } 113 | 114 | public static class ForDumpMethods { 115 | private SchemaDumper schemaDumper = new SchemaDumper(); 116 | 117 | @Test 118 | public void shouldDumpBySqlString() throws SQLException, IOException, InterruptedException { 119 | try { 120 | schemaDumper.dump(SQL_FOR_TEST); 121 | assertTrue(true); 122 | } catch (CommunicationsException e) { 123 | assumeTrue("MySQL maybe not launched", false); 124 | } catch (Exception e) { 125 | assertTrue(false); 126 | } 127 | } 128 | 129 | @Test 130 | public void shouldDumpBySqlFileWithDefaultCharset() 131 | throws IOException, SQLException, InterruptedException { 132 | File sqlFile = File.createTempFile("tempsql", ".sql"); 133 | 134 | try (BufferedWriter bufferedWriter = 135 | new BufferedWriter(new OutputStreamWriter(new FileOutputStream(sqlFile), 136 | Charset.forName("UTF-8")))) { 137 | bufferedWriter.write(SQL_FOR_TEST); 138 | } 139 | 140 | try { 141 | schemaDumper.dump(sqlFile); 142 | assertTrue(true); 143 | } catch (CommunicationsException e) { 144 | assumeTrue("MySQL maybe not launched", false); 145 | } catch (Exception e) { 146 | assertTrue(false); 147 | } finally { 148 | sqlFile.delete(); 149 | } 150 | } 151 | 152 | @Test 153 | public void shouldDumpBySqlFileWithSpecifiedCharset() 154 | throws IOException, SQLException, InterruptedException { 155 | File sqlFile = File.createTempFile("tempsql", ".sql"); 156 | 157 | try (BufferedWriter bufferedWriter = 158 | new BufferedWriter(new OutputStreamWriter(new FileOutputStream(sqlFile), 159 | Charset.forName("EUC-JP")))) { 160 | bufferedWriter.write(SQL_FOR_TEST); 161 | } 162 | 163 | try { 164 | schemaDumper.dump(sqlFile, Charset.forName("EUC-JP")); 165 | assertTrue(true); 166 | } catch (CommunicationsException e) { 167 | assumeTrue("MySQL maybe not launched", false); 168 | } catch (Exception e) { 169 | assertTrue(false); 170 | } finally { 171 | sqlFile.delete(); 172 | } 173 | 174 | } 175 | 176 | @Test 177 | public void shouldDumpFromLocalMySql() 178 | throws SQLException, IOException, InterruptedException { 179 | MySqlConnectionInfo connInfo = MySqlConnectionInfo.builder().build(); 180 | 181 | String tempDbName = new StringBuilder() 182 | .append("tmp_") 183 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 184 | .toString(); 185 | String mysqlUrl = connInfo.getJdbcUrl(); 186 | String user = connInfo.getUser(); 187 | String pass = connInfo.getPass(); 188 | 189 | try (Connection connection = DriverManager.getConnection(mysqlUrl, user, pass)) { 190 | try (Statement stmt = connection.createStatement()) { 191 | stmt.executeUpdate("CREATE DATABASE " + tempDbName); 192 | } 193 | try (Statement stmt = connection.createStatement()) { 194 | stmt.executeUpdate("USE " + tempDbName + "; " + SQL_FOR_TEST); 195 | } 196 | 197 | schemaDumper.dumpFromLocalDb(tempDbName); 198 | } catch (Exception e) { 199 | throw e; 200 | } finally { 201 | try (Connection connectionToTeardown = DriverManager.getConnection(mysqlUrl, user, pass)) { 202 | try (Statement stmt = connectionToTeardown.createStatement()) { 203 | stmt.executeUpdate("DROP DATABASE " + tempDbName); 204 | } 205 | } catch (CommunicationsException e) { 206 | assumeTrue("MySQL maybe not launched", false); 207 | } 208 | } 209 | assertTrue(true); 210 | } 211 | 212 | @Test 213 | public void shouldDumpFromRemoteMySql() throws SQLException, IOException, InterruptedException { 214 | MySqlConnectionInfo connInfo = MySqlConnectionInfo.builder().build(); 215 | 216 | String tempDbName = new StringBuilder() 217 | .append("tmp_") 218 | .append(UUID.randomUUID().toString().replaceAll("-", "")) 219 | .toString(); 220 | String mysqlUrl = connInfo.getJdbcUrl(); 221 | String user = connInfo.getUser(); 222 | String pass = connInfo.getPass(); 223 | 224 | try (Connection connection = DriverManager.getConnection(mysqlUrl, user, pass)) { 225 | try (Statement stmt = connection.createStatement()) { 226 | stmt.executeUpdate("CREATE DATABASE " + tempDbName); 227 | } 228 | try (Statement stmt = connection.createStatement()) { 229 | stmt.executeUpdate("USE " + tempDbName + "; " + SQL_FOR_TEST); 230 | } 231 | schemaDumper.dumpFromRemoteDb(tempDbName, connInfo); 232 | } catch (Exception e) { 233 | throw e; 234 | } finally { 235 | try (Connection connectionToTeardown = DriverManager.getConnection(mysqlUrl, user, pass)) { 236 | try (Statement stmt = connectionToTeardown.createStatement()) { 237 | stmt.executeUpdate("DROP DATABASE " + tempDbName); 238 | } 239 | } catch (CommunicationsException e) { 240 | assumeTrue("MySQL maybe not launched", false); 241 | } 242 | } 243 | assertTrue(true); 244 | } 245 | } 246 | } 247 | --------------------------------------------------------------------------------