├── .github └── workflows │ └── maven.yml ├── .gitignore ├── LICENSE ├── README.org ├── binary └── sql-formatter-1.0.0-jar-with-dependencies.jar ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── github │ │ └── sambatriste │ │ ├── CustomFormatter.java │ │ └── SqlFormatter.java └── script │ └── sql-formatter.groovy └── test └── java └── com └── github └── sambatriste └── SqlFormatterTest.java /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build with Maven 24 | run: mvn -B package --file pom.xml 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Maven template 3 | target/ 4 | pom.xml.tag 5 | pom.xml.releaseBackup 6 | pom.xml.versionsBackup 7 | pom.xml.next 8 | release.properties 9 | dependency-reduced-pom.xml 10 | buildNumber.properties 11 | .mvn/timing.properties 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 KAWASAKI Tsuyoshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | #+TITLE: SQL formatter 2 | 3 | ** これは何? 4 | 5 | SQL文のフォーマットを行います。 6 | 標準入力から受け取ったSQL文をフォーマットして、標準出力に出力します。 7 | 8 | *** 使用例 9 | #+BEGIN_SRC sh 10 | echo "SELECT * FROM HOGE WHERE HOGE.FUGA = :fuga" | java -jar target/sql-formatter-1.0.1-jar-with-dependencies.jar 11 | 12 | #+END_SRC 13 | 14 | *** 出力結果 15 | 16 | #+BEGIN_EXAMPLE 17 | SELECT 18 | * 19 | FROM 20 | HOGE 21 | WHERE 22 | HOGE.FUGA = :fuga 23 | #+END_EXAMPLE 24 | 25 | *** 依存ライブラリ 26 | 27 | Hibernateのorg.hibernate.engine.jdbc.internal.BasicFormatterImplを使用しています。 28 | 29 | *** 参考URL 30 | 31 | [[http://stackoverflow.com/questions/312552/looking-for-an-embeddable-sql-beautifier-or-reformatter][- Looking for an embeddable SQL beautifier or reformatter]] 32 | 33 | 34 | ** バイナリ 35 | 36 | ビルドが面倒な場合は、binaryディレクトリ配下のjarをダウンロードしてください。 37 | 38 | ** Emacsとの連携 39 | 40 | Emacsでは、リージョン内の内容を外部コマンドに渡して、その結果でバッファを書き換えることができます。 41 | これを利用してSQL文の整形をEmacs上で行います。 42 | 43 | #+BEGIN_SRC lisp 44 | ;;; SQL文の整形をする設定 45 | ;; 実行する外部コマンド 46 | (setq sql-format-external-command 47 | (concat "java -jar " (expand-file-name "~/.emacs.d/lib/sql-formatter-1.0.0-jar-with-dependencies.jar"))) 48 | 49 | ;; SQL文をフォーマットする関数 50 | (defun my-format-sql () 51 | "バッファまたはリージョン内のSQL文を整形する。" 52 | (interactive) 53 | (let (begin end) 54 | (cond (mark-active 55 | (setq begin (region-beginning)) 56 | (setq end (region-end))) 57 | (t 58 | (setq begin (point-min)) 59 | (setq end (point-max)))) 60 | (save-excursion 61 | (shell-command-on-region 62 | begin 63 | end 64 | sql-format-external-command 65 | nil 66 | t ; replace buffer 67 | )))) 68 | 69 | ;; キーバインド設定 70 | (with-eval-after-load "sql" 71 | (define-key sql-mode-map (kbd "C-S-f") 'my-format-sql)) 72 | #+END_SRC 73 | -------------------------------------------------------------------------------- /binary/sql-formatter-1.0.0-jar-with-dependencies.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambatriste/sql-formatter/8674a8be78262b3817810cc4150b32361e5536ea/binary/sql-formatter-1.0.0-jar-with-dependencies.jar -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.github.sambatriste 5 | sql-formatter 6 | jar 7 | 1.0.1 8 | SQL formatter 9 | https://github.com/sambatriste/sql-formatter 10 | 11 | 12 | UTF-8 13 | 1.7 14 | 1.7 15 | 16 | 17 | 18 | 19 | org.hibernate 20 | hibernate-core 21 | 5.4.28.Final 22 | 23 | 24 | junit 25 | junit 26 | 4.13.1 27 | test 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.apache.maven.plugins 35 | maven-compiler-plugin 36 | 3.6.2 37 | 38 | 39 | org.apache.maven.plugins 40 | maven-assembly-plugin 41 | 2.5.1 42 | 43 | 44 | make-assembly 45 | package 46 | 47 | single 48 | 49 | 50 | 51 | 52 | 53 | jar-with-dependencies 54 | 55 | 56 | 57 | com.github.sambatriste.SqlFormatter 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/github/sambatriste/CustomFormatter.java: -------------------------------------------------------------------------------- 1 | package com.github.sambatriste; 2 | 3 | 4 | import org.hibernate.engine.jdbc.internal.Formatter; 5 | 6 | import java.util.HashSet; 7 | import java.util.LinkedList; 8 | import java.util.Locale; 9 | import java.util.Set; 10 | import java.util.StringTokenizer; 11 | 12 | import org.hibernate.internal.util.StringHelper; 13 | 14 | /** 15 | * {@link org.hibernate.engine.jdbc.internal.BasicFormatterImpl}をカスタマイズしたフォーマッタ。 16 | * 17 | *
 18 |  * | 項目           | BasicFormatterImpl | CustomFormatter |
 19 |  * |----------------|--------------------|-----------------|
 20 |  * | インデント     | 4スペース          | 2スペース       |
 21 |  * | 初期インデント | 1                  | 0               |
 22 |  * | 開始行         | 改行あり           | 改行なし        |
 23 |  * 
24 | * 25 | * @see org.hibernate.engine.jdbc.internal.BasicFormatterImpl 26 | */ 27 | public class CustomFormatter implements Formatter { 28 | 29 | private static final Set BEGIN_CLAUSES = new HashSet(); 30 | private static final Set END_CLAUSES = new HashSet(); 31 | private static final Set LOGICAL = new HashSet(); 32 | private static final Set QUANTIFIERS = new HashSet(); 33 | private static final Set DML = new HashSet(); 34 | private static final Set MISC = new HashSet(); 35 | 36 | static { 37 | BEGIN_CLAUSES.add( "left" ); 38 | BEGIN_CLAUSES.add( "right" ); 39 | BEGIN_CLAUSES.add( "inner" ); 40 | BEGIN_CLAUSES.add( "outer" ); 41 | BEGIN_CLAUSES.add( "group" ); 42 | BEGIN_CLAUSES.add( "order" ); 43 | 44 | END_CLAUSES.add( "where" ); 45 | END_CLAUSES.add( "set" ); 46 | END_CLAUSES.add( "having" ); 47 | END_CLAUSES.add( "from" ); 48 | END_CLAUSES.add( "by" ); 49 | END_CLAUSES.add( "join" ); 50 | END_CLAUSES.add( "into" ); 51 | END_CLAUSES.add( "union" ); 52 | 53 | LOGICAL.add( "and" ); 54 | LOGICAL.add( "or" ); 55 | LOGICAL.add( "when" ); 56 | LOGICAL.add( "else" ); 57 | LOGICAL.add( "end" ); 58 | 59 | QUANTIFIERS.add( "in" ); 60 | QUANTIFIERS.add( "all" ); 61 | QUANTIFIERS.add( "exists" ); 62 | QUANTIFIERS.add( "some" ); 63 | QUANTIFIERS.add( "any" ); 64 | 65 | DML.add( "insert" ); 66 | DML.add( "update" ); 67 | DML.add( "delete" ); 68 | 69 | MISC.add( "select" ); 70 | MISC.add( "on" ); 71 | } 72 | 73 | //private static final String INDENT_STRING = " "; 74 | private static final String INDENT_STRING = " "; 75 | //private static final String INITIAL = System.lineSeparator() + INDENT_STRING; 76 | private static final String INITIAL = ""; 77 | 78 | @Override 79 | public String format(String source) { 80 | return new FormatProcess( source ).perform(); 81 | } 82 | 83 | private static class FormatProcess { 84 | boolean beginLine = true; 85 | boolean afterBeginBeforeEnd; 86 | boolean afterByOrSetOrFromOrSelect; 87 | boolean afterOn; 88 | boolean afterBetween; 89 | boolean afterInsert; 90 | int inFunction; 91 | int parensSinceSelect; 92 | private LinkedList parenCounts = new LinkedList(); 93 | private LinkedList afterByOrFromOrSelects = new LinkedList(); 94 | 95 | //int indent = 1; 96 | int indent = 0; 97 | 98 | StringBuilder result = new StringBuilder(); 99 | StringTokenizer tokens; 100 | String lastToken; 101 | String token; 102 | String lcToken; 103 | 104 | public FormatProcess(String sql) { 105 | tokens = new StringTokenizer( 106 | sql, 107 | "()+*/-=<>'`\"[]," + StringHelper.WHITESPACE, 108 | true 109 | ); 110 | } 111 | 112 | public String perform() { 113 | 114 | result.append( INITIAL ); 115 | 116 | while ( tokens.hasMoreTokens() ) { 117 | token = tokens.nextToken(); 118 | lcToken = token.toLowerCase(Locale.ROOT); 119 | 120 | if ( "'".equals( token ) ) { 121 | String t; 122 | do { 123 | t = tokens.nextToken(); 124 | token += t; 125 | } 126 | // cannot handle single quotes 127 | while ( !"'".equals( t ) && tokens.hasMoreTokens() ); 128 | } 129 | else if ( "\"".equals( token ) ) { 130 | String t; 131 | do { 132 | t = tokens.nextToken(); 133 | token += t; 134 | } 135 | while ( !"\"".equals( t ) && tokens.hasMoreTokens() ); 136 | } 137 | // SQL Server uses "[" and "]" to escape reserved words 138 | // see SQLServerDialect.openQuote and SQLServerDialect.closeQuote 139 | else if ( "[".equals( token ) ) { 140 | String t; 141 | do { 142 | t = tokens.nextToken(); 143 | token += t; 144 | } 145 | while ( !"]".equals( t ) && tokens.hasMoreTokens()); 146 | } 147 | 148 | if ( afterByOrSetOrFromOrSelect && ",".equals( token ) ) { 149 | commaAfterByOrFromOrSelect(); 150 | } 151 | else if ( afterOn && ",".equals( token ) ) { 152 | commaAfterOn(); 153 | } 154 | 155 | else if ( "(".equals( token ) ) { 156 | openParen(); 157 | } 158 | else if ( ")".equals( token ) ) { 159 | closeParen(); 160 | } 161 | 162 | else if ( BEGIN_CLAUSES.contains( lcToken ) ) { 163 | beginNewClause(); 164 | } 165 | 166 | else if ( END_CLAUSES.contains( lcToken ) ) { 167 | endNewClause(); 168 | } 169 | 170 | else if ( "select".equals( lcToken ) ) { 171 | select(); 172 | } 173 | 174 | else if ( DML.contains( lcToken ) ) { 175 | updateOrInsertOrDelete(); 176 | } 177 | 178 | else if ( "values".equals( lcToken ) ) { 179 | values(); 180 | } 181 | 182 | else if ( "on".equals( lcToken ) ) { 183 | on(); 184 | } 185 | 186 | else if ( afterBetween && lcToken.equals( "and" ) ) { 187 | misc(); 188 | afterBetween = false; 189 | } 190 | 191 | else if ( LOGICAL.contains( lcToken ) ) { 192 | logical(); 193 | } 194 | 195 | else if ( isWhitespace( token ) ) { 196 | white(); 197 | } 198 | 199 | else { 200 | misc(); 201 | } 202 | 203 | if ( !isWhitespace( token ) ) { 204 | lastToken = lcToken; 205 | } 206 | 207 | } 208 | return result.toString(); 209 | } 210 | 211 | private void commaAfterOn() { 212 | out(); 213 | indent--; 214 | newline(); 215 | afterOn = false; 216 | afterByOrSetOrFromOrSelect = true; 217 | } 218 | 219 | private void commaAfterByOrFromOrSelect() { 220 | out(); 221 | newline(); 222 | } 223 | 224 | private void logical() { 225 | if ( "end".equals( lcToken ) ) { 226 | indent--; 227 | } 228 | newline(); 229 | out(); 230 | beginLine = false; 231 | } 232 | 233 | private void on() { 234 | indent++; 235 | afterOn = true; 236 | newline(); 237 | out(); 238 | beginLine = false; 239 | } 240 | 241 | private void misc() { 242 | out(); 243 | if ( "between".equals( lcToken ) ) { 244 | afterBetween = true; 245 | } 246 | if ( afterInsert ) { 247 | newline(); 248 | afterInsert = false; 249 | } 250 | else { 251 | beginLine = false; 252 | if ( "case".equals( lcToken ) ) { 253 | indent++; 254 | } 255 | } 256 | } 257 | 258 | private void white() { 259 | if ( !beginLine ) { 260 | result.append( " " ); 261 | } 262 | } 263 | 264 | private void updateOrInsertOrDelete() { 265 | out(); 266 | indent++; 267 | beginLine = false; 268 | if ( "update".equals( lcToken ) ) { 269 | newline(); 270 | } 271 | if ( "insert".equals( lcToken ) ) { 272 | afterInsert = true; 273 | } 274 | } 275 | 276 | private void select() { 277 | out(); 278 | indent++; 279 | newline(); 280 | parenCounts.addLast( parensSinceSelect ); 281 | afterByOrFromOrSelects.addLast( afterByOrSetOrFromOrSelect ); 282 | parensSinceSelect = 0; 283 | afterByOrSetOrFromOrSelect = true; 284 | } 285 | 286 | private void out() { 287 | result.append( token ); 288 | } 289 | 290 | private void endNewClause() { 291 | if ( !afterBeginBeforeEnd ) { 292 | indent--; 293 | if ( afterOn ) { 294 | indent--; 295 | afterOn = false; 296 | } 297 | newline(); 298 | } 299 | out(); 300 | if ( !"union".equals( lcToken ) ) { 301 | indent++; 302 | } 303 | newline(); 304 | afterBeginBeforeEnd = false; 305 | afterByOrSetOrFromOrSelect = "by".equals( lcToken ) 306 | || "set".equals( lcToken ) 307 | || "from".equals( lcToken ); 308 | } 309 | 310 | private void beginNewClause() { 311 | if ( !afterBeginBeforeEnd ) { 312 | if ( afterOn ) { 313 | indent--; 314 | afterOn = false; 315 | } 316 | indent--; 317 | newline(); 318 | } 319 | out(); 320 | beginLine = false; 321 | afterBeginBeforeEnd = true; 322 | } 323 | 324 | private void values() { 325 | indent--; 326 | newline(); 327 | out(); 328 | indent++; 329 | newline(); 330 | } 331 | 332 | private void closeParen() { 333 | parensSinceSelect--; 334 | if ( parensSinceSelect < 0 ) { 335 | indent--; 336 | parensSinceSelect = parenCounts.removeLast(); 337 | afterByOrSetOrFromOrSelect = afterByOrFromOrSelects.removeLast(); 338 | } 339 | if ( inFunction > 0 ) { 340 | inFunction--; 341 | out(); 342 | } 343 | else { 344 | if ( !afterByOrSetOrFromOrSelect ) { 345 | indent--; 346 | newline(); 347 | } 348 | out(); 349 | } 350 | beginLine = false; 351 | } 352 | 353 | private void openParen() { 354 | if ( isFunctionName( lastToken ) || inFunction > 0 ) { 355 | inFunction++; 356 | } 357 | beginLine = false; 358 | if ( inFunction > 0 ) { 359 | out(); 360 | } 361 | else { 362 | out(); 363 | if ( !afterByOrSetOrFromOrSelect ) { 364 | indent++; 365 | newline(); 366 | beginLine = true; 367 | } 368 | } 369 | parensSinceSelect++; 370 | } 371 | 372 | private static boolean isFunctionName(String tok) { 373 | if ( tok == null || tok.length() == 0 ) { 374 | return false; 375 | } 376 | 377 | final char begin = tok.charAt( 0 ); 378 | final boolean isIdentifier = Character.isJavaIdentifierStart( begin ) || '"' == begin; 379 | return isIdentifier && 380 | !LOGICAL.contains( tok ) && 381 | !END_CLAUSES.contains( tok ) && 382 | !QUANTIFIERS.contains( tok ) && 383 | !DML.contains( tok ) && 384 | !MISC.contains( tok ); 385 | } 386 | 387 | private static boolean isWhitespace(String token) { 388 | return StringHelper.WHITESPACE.contains( token ); 389 | } 390 | 391 | private void newline() { 392 | result.append( System.lineSeparator() ); 393 | for ( int i = 0; i < indent; i++ ) { 394 | result.append( INDENT_STRING ); 395 | } 396 | beginLine = true; 397 | } 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /src/main/java/com/github/sambatriste/SqlFormatter.java: -------------------------------------------------------------------------------- 1 | package com.github.sambatriste; 2 | 3 | import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; 4 | import org.hibernate.engine.jdbc.internal.Formatter; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.InputStreamReader; 9 | import java.io.OutputStream; 10 | import java.io.OutputStreamWriter; 11 | import java.io.Reader; 12 | import java.io.Writer; 13 | 14 | public class SqlFormatter { 15 | 16 | private final Reader reader; 17 | 18 | private final Writer writer; 19 | 20 | public static void main(String[] args) throws IOException { 21 | SqlFormatter sqlFormatter = new SqlFormatter(System.in, System.out); 22 | sqlFormatter.format(); 23 | } 24 | 25 | public SqlFormatter(Reader reader, Writer writer) { 26 | this.reader = reader; 27 | this.writer = writer; 28 | } 29 | 30 | public SqlFormatter(InputStream in, OutputStream out) { 31 | this(new InputStreamReader(in), new OutputStreamWriter(out)); 32 | } 33 | 34 | public void format() throws IOException { 35 | String original = readSql(); 36 | String formatted = newFormatter().format(original); 37 | writer.write(formatted); 38 | writer.flush(); 39 | } 40 | 41 | private String readSql() throws IOException { 42 | int c; 43 | StringBuilder sql = new StringBuilder(1024); 44 | while ((c = reader.read()) != -1) { 45 | sql.append((char) c); 46 | } 47 | return sql.toString(); 48 | 49 | } 50 | 51 | private Formatter newFormatter() { 52 | return new CustomFormatter(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/script/sql-formatter.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * 標準入力からSQL文を読み取り、整形したSQL文を標準出力に出力します。 3 | */ 4 | 5 | @Grab(group='org.hibernate', module='hibernate-core', version='5.2.3.Final') 6 | import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; 7 | 8 | def formatter = new BasicFormatterImpl() 9 | 10 | String original = System.in.text 11 | String formatted = formatter.format(original) 12 | println formatted -------------------------------------------------------------------------------- /src/test/java/com/github/sambatriste/SqlFormatterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.sambatriste; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.io.StringReader; 7 | import java.io.StringWriter; 8 | 9 | import static org.hamcrest.CoreMatchers.is; 10 | import static org.junit.Assert.assertThat; 11 | 12 | public class SqlFormatterTest { 13 | 14 | @Test 15 | public void test() throws IOException { 16 | String original = "SELECT * FROM HOGE WHERE HOGE.FUGA = :fuga"; 17 | String expected = 18 | "SELECT\n" + 19 | " * \n" + 20 | "FROM\n" + 21 | " HOGE \n" + 22 | "WHERE\n" + 23 | " HOGE.FUGA = :fuga"; 24 | String formatted = format(original); 25 | assertThat(formatted, is(expected)); 26 | 27 | } 28 | 29 | private String format(String sql) throws IOException { 30 | StringReader reader = new StringReader(sql); 31 | StringWriter writer = new StringWriter(); 32 | SqlFormatter sut = new SqlFormatter(reader, writer); 33 | sut.format(); 34 | return writer.toString(); 35 | 36 | } 37 | 38 | } --------------------------------------------------------------------------------